From 14af1a096744a32671ae06973359ae810ac62449 Mon Sep 17 00:00:00 2001 From: Parham Moieni Date: Wed, 7 Sep 2022 18:16:47 +0430 Subject: [PATCH 001/246] basic server scaffold --- .gitignore | 1 + api/auth.go | 652 +++++++++++++++++++++++ api/errors.go | 28 + api/internal/db/migration/schema.sql | 12 + api/internal/db/user/db.go | 138 +++++ api/internal/db/user/models.go | 20 + api/internal/db/user/query.sql | 54 ++ api/internal/db/user/query.sql.go | 168 ++++++ api/internal/db/user/schema.sql | 11 + api/jam.go | 171 +++++++ api/middlewares.go | 37 ++ api/server.go | 66 +++ api/utils.go | 31 ++ client/.gitignore | 24 + client/README.md | 48 ++ client/index.html | 13 + client/package.json | 23 + client/public/vite.svg | 1 + client/src/App.svelte | 9 + client/src/assets/svelte.svg | 1 + client/src/main.ts | 8 + client/src/vite-env.d.ts | 2 + client/svelte.config.js | 7 + client/tsconfig.json | 21 + client/tsconfig.node.json | 8 + client/vite.config.ts | 7 + client/yarn.lock | 733 ++++++++++++++++++++++++++ cmd/main.go | 8 - cmd/server/main.go | 99 +++- config.go | 18 + go.mod | 46 ++ go.sum | 738 +++++++++++++++++++++++++++ sqlc.yaml | 15 + 33 files changed, 3207 insertions(+), 11 deletions(-) create mode 100644 api/auth.go create mode 100644 api/errors.go create mode 100644 api/internal/db/migration/schema.sql create mode 100644 api/internal/db/user/db.go create mode 100644 api/internal/db/user/models.go create mode 100644 api/internal/db/user/query.sql create mode 100644 api/internal/db/user/query.sql.go create mode 100644 api/internal/db/user/schema.sql create mode 100644 api/jam.go create mode 100644 api/middlewares.go create mode 100644 api/server.go create mode 100644 api/utils.go create mode 100644 client/.gitignore create mode 100644 client/README.md create mode 100644 client/index.html create mode 100644 client/package.json create mode 100644 client/public/vite.svg create mode 100644 client/src/App.svelte create mode 100644 client/src/assets/svelte.svg create mode 100644 client/src/main.ts create mode 100644 client/src/vite-env.d.ts create mode 100644 client/svelte.config.js create mode 100644 client/tsconfig.json create mode 100644 client/tsconfig.node.json create mode 100644 client/vite.config.ts create mode 100644 client/yarn.lock delete mode 100644 cmd/main.go create mode 100644 config.go create mode 100644 go.sum create mode 100644 sqlc.yaml diff --git a/.gitignore b/.gitignore index d31169fa..dd593eaf 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ bin/ # Other .DS_Store +*.env diff --git a/api/auth.go b/api/auth.go new file mode 100644 index 00000000..2797977d --- /dev/null +++ b/api/auth.go @@ -0,0 +1,652 @@ +package api + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "log" + "net/http" + "net/mail" + "time" + "unicode" + + "github.com/go-redis/redis/v9" + "github.com/go-sql-driver/mysql" + "github.com/google/uuid" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/rog-golang-buddies/rapidmidiex/api/internal/db/user" + "github.com/spf13/viper" + "golang.org/x/crypto/bcrypt" +) + +type AuthService struct { + DBCon *sql.DB + RedisRefreshTokenDB *redis.Client + RedisClientIDDB *redis.Client + RedisPasswordTokenDB *redis.Client +} + +const ( + authorizationHeader = "Authorization" + refreshTokenCookieName = "RMX_DIRECT_RT" + refreshTokenCookiePath = "/api/v1" +) + +type loginRes struct { + IDToken string `json:"id_token"` + AccessToken string `json:"access_token"` +} + +type refreshTokenRes struct { + AccessToken string `json:"access_token"` +} + +type userInfoRes struct { + Username string `json:"username"` + Email string `json:"email"` +} + +func (s *AuthService) Login(w http.ResponseWriter, r *http.Request) { + // get user credentials from request and bind it to UserLoginCreds type + user := user.User{} + if err := parse(r, &user); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Login the user with provided credentials + tokens, err := s.login(&user) + handlerError(w, err) + + rtCookie := http.Cookie{ + Path: refreshTokenCookiePath, + Name: refreshTokenCookieName, + Value: tokens.refreshToken, + HttpOnly: true, + Secure: false, // set to true in production + SameSite: http.SameSiteLaxMode, + Expires: time.Now().UTC().Add(refreshTokenExpiry), + } + + res := loginRes{ + IDToken: tokens.idToken, + AccessToken: tokens.accessToken, + } + + http.SetCookie(w, &rtCookie) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(res) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } +} + +func (s *AuthService) Register(w http.ResponseWriter, r *http.Request) { + user := user.User{} + if err := parse(r, &user); err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + err := s.register(&user) + handlerError(w, err) + + w.WriteHeader(http.StatusOK) +} + +func (s *AuthService) RefreshToken(w http.ResponseWriter, r *http.Request) { + rtCookie, err := r.Cookie(refreshTokenCookieName) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + tokens, err := s.refreshToken(rtCookie.Value) + handlerError(w, err) + + newRTCookie := http.Cookie{ + Path: refreshTokenCookiePath, + Name: refreshTokenCookieName, + Value: tokens.refreshToken, + HttpOnly: true, + Secure: false, // set to true in production + SameSite: http.SameSiteLaxMode, + Expires: time.Now().UTC().Add(refreshTokenExpiry), + } + res := refreshTokenRes{ + AccessToken: tokens.accessToken, + } + + http.SetCookie(w, &newRTCookie) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(res) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } +} + +func (s *AuthService) Logout(w http.ResponseWriter, r *http.Request) { + // remove refresh token cookie + cookie := &http.Cookie{ + Path: refreshTokenCookiePath, + Name: refreshTokenCookieName, + Value: "", + HttpOnly: true, + Secure: false, // set to true in production + SameSite: http.SameSiteLaxMode, + Expires: time.Unix(0, 0), + } + + http.SetCookie(w, cookie) + w.WriteHeader(http.StatusOK) +} + +// func (s *AuthService) GetUsersEmailList(w http.ResponseWriter, r *http.Request) { +// q := auth.New(s.DBCon) +// users, err := q.ListUsers(context.Background()) +// if err != nil { +// log.Println(err) +// w.WriteHeader(http.StatusInternalServerError) +// return +// } + +// res := usersEmailListRes{} +// for _, user := range users { +// res.Users = append(res.Users, user.Email) +// } + +// w.Header().Set("Content-Type", "application/json") +// w.WriteHeader(http.StatusOK) +// err = json.NewEncoder(w).Encode(res) +// if err != nil { +// log.Println(err) +// w.WriteHeader(http.StatusInternalServerError) +// return +// } +// } + +func (s *AuthService) GetUserInfo(w http.ResponseWriter, r *http.Request) { + email, ok := r.Context().Value(emailCtxKey).(string) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + + user, err := s.getUserInfo(email) + handlerError(w, err) + + res := userInfoRes{} + res.Username = user.Username + res.Email = user.Email + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(res) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } +} + +func (s *AuthService) UpdateUserInfo(w http.ResponseWriter, r *http.Request) { + email, ok := r.Context().Value(emailCtxKey).(string) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + + newUserInfo := user.User{} + if err := parse(r, &newUserInfo); err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + err := s.updateUserInfo(email, &newUserInfo) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// ------------------------ SERVICE ------------------------------- +type ( + authTokens struct { + idToken string + accessToken string + refreshToken string + } + refreshTokens struct { + accessToken string + refreshToken string + } + idTokenClaims struct { + Email string `json:"email"` + // emailVerified bool + } + accessTokenClaims struct { + Email string `json:"email"` + ClientID string `json:"client_id"` + } + refreshTokenClaims struct { + Email string `json:"email"` + ClientID string `json:"client_id"` + } +) + +const ( + idTokenExpiry = time.Hour * 10 + accessTokenExpiry = time.Minute * 5 + refreshTokenExpiry = time.Hour * 24 * 7 +) + +func (s *AuthService) register(u *user.User) error { + if isEmptyString(u.Username) { + return &errInvalidRegisterInfo + } + + err := validateEmail(u.Email) + if err != nil { + return err + } + + err = validatePassword(u.Password) + if err != nil { + return err + } + + userInfo := user.CreateUserParams{} + userInfo.Username = u.Username + userInfo.Email = u.Email + // u.EmailVerified = false // default value when user registers for the first time + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + userInfo.Password = string(hashedPassword) + + ctx := context.Background() + q := user.New(s.DBCon) + _, err = q.CreateUser(ctx, &userInfo) + var mysqlErr *mysql.MySQLError + if err != nil { + if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { + return &errUserAlreadyExists + } + return err + } + + return nil +} + +func (s *AuthService) login(u *user.User) (authTokens, error) { + // check if provided email is valid + err := validateEmail(u.Email) + if err != nil { + return authTokens{}, err + } + + // get user info from database to create new authentication tokens + ctx := context.Background() + q := user.New(s.DBCon) + user, err := q.GetUserByEmail(ctx, u.Email) + if err != nil { + return authTokens{}, err + } + + // check user password + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(u.Password)) + if err != nil { + // if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { + // err = &models.ErrorResponse{Status: http.StatusUnauthorized, Message: errWrongPassword} + // } + // if errors.Is(err, bcrypt.ErrHashTooShort) { + // err = &models.ErrorResponse{Status: http.StatusUnauthorized, Message: errWrongPassword} + // } + return authTokens{}, err + } + + clientID := uuid.New().String() + idClaims := idTokenClaims{ + Email: user.Email, + // emailVerified: u.EmailVerified, + } + it, err := genIDToken(&idClaims) + if err != nil { + return authTokens{}, err + } + + accessClaims := accessTokenClaims{ + Email: user.Email, + ClientID: clientID, + } + at, err := genAccessToken(&accessClaims) + if err != nil { + return authTokens{}, err + } + + refreshClaims := refreshTokenClaims{ + Email: user.Email, + ClientID: clientID, + } + rt, err := genRefreshToken(&refreshClaims) + if err != nil { + return authTokens{}, err + } + + tokens := authTokens{ + idToken: it, + accessToken: at, + refreshToken: rt, + } + return tokens, nil +} + +func (s *AuthService) refreshToken(rt string) (refreshTokens, error) { + // validate and parse the refresh token + rtPayload, err := parseRefreshTokenWithValidate(rt) + if err != nil { + return refreshTokens{}, err + } + + privateClaims := rtPayload.PrivateClaims() + + email, ok := privateClaims["email"].(string) + if !ok { + return refreshTokens{}, &errorResponse{status: http.StatusUnauthorized, message: http.StatusText(http.StatusUnauthorized)} + } + + // check for token reuse + reuse, err := s.isTokenUsed(rt) + if err != nil { + return refreshTokens{}, err + } + if reuse { + return refreshTokens{}, &errorResponse{status: http.StatusUnauthorized, message: http.StatusText(http.StatusUnauthorized)} + } + + // save used token in database to detect token reuse when this toke is used again + // since we have passed the token reuse check, this is the first time the token is being used + // so we save it in used refresh tokens list to detect future token reuses + err = s.saveRefreshToken(rt, email, rtPayload.Subject()) + if err != nil { + return refreshTokens{}, err + } + + // if the client id is revoked then the token is invalid and is reused by malicious user + revoked, err := s.isClientIDRevoked(rtPayload.Subject()) + if err != nil { + return refreshTokens{}, err + } + if revoked { + return refreshTokens{}, &errorResponse{status: http.StatusUnauthorized, message: http.StatusText(http.StatusUnauthorized)} + } + + // generate new access token from previous access token claims + newATClaims := accessTokenClaims{ + Email: email, + ClientID: rtPayload.Subject(), + } + newAT, err := genAccessToken(&newATClaims) + if err != nil { + return refreshTokens{}, err + } + + // generate new refresh token form previous access token claims + newRTClaims := refreshTokenClaims{ + Email: email, + ClientID: rtPayload.Subject(), + } + newRT, err := genRefreshToken(&newRTClaims) + if err != nil { + return refreshTokens{}, err + } + + tokens := refreshTokens{ + accessToken: newAT, + refreshToken: newRT, + } + + return tokens, nil +} + +func (s *AuthService) saveRefreshToken(token, email, clientID string) error { + t := refreshTokenClaims{ + Email: email, + ClientID: clientID, + } + b, err := json.Marshal(&t) + if err != nil { + return err + } + payload, err := parseRefreshToken(token) + if err != nil { + return err + } + d := time.Now().UTC().Sub(payload.IssuedAt()) + + _, err = s.RedisRefreshTokenDB.Set(context.Background(), token, string(b), d).Result() + if err != nil { + return err + } + + return nil +} + +func (s *AuthService) isTokenUsed(token string) (bool, error) { + // check if token is available in redis database + // if it's not then token is not reused + v, err := s.RedisRefreshTokenDB.Get(context.Background(), token).Result() + if err != nil { + switch err { + case redis.Nil: + return false, nil + default: + return false, err + } + } + + // token is available in redis database which means it's reused + // get token information containing client id and email of user + t := refreshTokenClaims{} + err = json.Unmarshal([]byte(v), &t) + if err != nil { + return false, err + } + + // save client id in redis database to deny any refresh token with the sub value of revoked client id + err = s.revokeClientID(t.ClientID, t.Email) + if err != nil { + return false, err + } + + return true, nil +} + +func (s *AuthService) revokeClientID(clientID, email string) error { + _, err := s.RedisClientIDDB.Set(context.Background(), clientID, email, refreshTokenExpiry).Result() + if err != nil { + return err + } + return nil +} + +func (s *AuthService) isClientIDRevoked(clientID string) (bool, error) { + // check if a key with client id exists + // if the key exists it means that the client id is revoked and token should be denied + // we don't need the email value here + _, err := s.RedisClientIDDB.Get(context.Background(), clientID).Result() + if err != nil { + switch err { + case redis.Nil: + return false, nil + default: + return false, err + } + } + + return true, nil +} + +func parseRefreshToken(token string) (payload jwt.Token, err error) { + payload, err = jwt.Parse([]byte(token), + jwt.WithKey(jwa.HS256, + []byte(viper.GetString("REFRESH_TOKEN_HMAC_SECRET")))) + return +} + +// checks access token validity and returns its payload +func parseAccessTokenWithValidate(token string) (payload jwt.Token, err error) { + payload, err = jwt.Parse([]byte(token), + jwt.WithKey(jwa.HS256, + []byte(viper.GetString("ACCESS_TOKEN_HMAC_SECRET"))), + jwt.WithValidate(true)) + return +} + +// checks refresh token validity and returns its payload +func parseRefreshTokenWithValidate(token string) (payload jwt.Token, err error) { + payload, err = jwt.Parse([]byte(token), + jwt.WithKey(jwa.HS256, + []byte(viper.GetString("REFRESH_TOKEN_HMAC_SECRET"))), + jwt.WithValidate(true)) + return +} + +func genIDToken(c *idTokenClaims) (string, error) { + token, err := jwt.NewBuilder(). + Issuer("http://localhost:8080"). + Subject(c.Email). + Audience([]string{"http://localhost:3000"}). + IssuedAt(time.Now().UTC()). + Expiration(time.Now().UTC().Add(idTokenExpiry)). + Claim("email", c.Email). + // Claim("email_verified", c.emailVerified). + Build() + if err != nil { + return "", err + } + + signed, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte(viper.GetString("ID_TOKEN_HMAC_SECRET")))) + if err != nil { + return "", err + } + + return string(signed), nil +} + +func genAccessToken(c *accessTokenClaims) (string, error) { + // scope := strings.Join(c.scope, " ") + token, err := jwt.NewBuilder(). + Issuer("http://localhost:8080"). + Subject(c.ClientID). + Audience([]string{"http://localhost:3000"}). + IssuedAt(time.Now().UTC()). + Expiration(time.Now().UTC().Add(accessTokenExpiry)). + Claim("email", c.Email). + Build() + if err != nil { + return "", err + } + + signed, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte(viper.GetString("ACCESS_TOKEN_HMAC_SECRET")))) + if err != nil { + return "", err + } + + return string(signed), nil +} + +func genRefreshToken(c *refreshTokenClaims) (string, error) { + token, err := jwt.NewBuilder(). + Issuer("http://localhost:8080"). + Subject(c.ClientID). + Audience([]string{"http://localhost:3000"}). + IssuedAt(time.Now().UTC()). + Expiration(time.Now().UTC().Add(refreshTokenExpiry)). + Claim("email", c.Email). + Build() + if err != nil { + return "", err + } + + signed, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte(viper.GetString("REFRESH_TOKEN_HMAC_SECRET")))) + if err != nil { + return "", err + } + + return string(signed), nil +} + +func validateEmail(e string) error { + if _, err := mail.ParseAddress(e); err != nil { + return &errInvalidEmail + } + return nil +} + +func validatePassword(p string) error { + var ( + hasMinLen = false + hasNumber = false + ) + if len(p) >= 8 { + hasMinLen = true + } + for _, char := range p { + if unicode.IsNumber(char) { + hasNumber = true + } + } + + if !(hasMinLen && hasNumber) { + return &errBadPassword + } + + return nil +} + +func (s *AuthService) getUserInfo(email string) (user.User, error) { + q := user.New(s.DBCon) + userInfo, err := q.GetUserByEmail(context.Background(), email) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + + } + return user.User{}, err + } + + return userInfo, nil +} + +func (s *AuthService) updateUserInfo(email string, u *user.User) error { + q := user.New(s.DBCon) + userInfo, err := q.GetUserByEmail(context.Background(), email) + if err != nil { + return err + } + + updateUser := user.UpdateUserParams{} + updateUser.ID = userInfo.ID + if isEmptyString(u.Username) { + return &errInvalidUpdateInfo + } + updateUser.Username = u.Username + + _, err = q.UpdateUser(context.Background(), &updateUser) + if err != nil { + return err + } + + return nil +} diff --git a/api/errors.go b/api/errors.go new file mode 100644 index 00000000..55694b4a --- /dev/null +++ b/api/errors.go @@ -0,0 +1,28 @@ +package api + +import ( + "log" + "net/http" +) + +var ( + errInvalidUpdateInfo = errorResponse{status: http.StatusBadRequest, message: "Invalid Username"} + errInvalidRegisterInfo = errorResponse{status: http.StatusBadRequest, message: "Invalid Register Info"} + errUserAlreadyExists = errorResponse{status: http.StatusForbidden, message: "User with the same email already exists."} + errUserNotFound = errorResponse{status: http.StatusNotFound, message: "User not found."} + errBadPassword = errorResponse{status: http.StatusBadRequest, message: "Password must contain at least 8 characters and one number"} + errInvalidEmail = errorResponse{status: http.StatusBadRequest, message: "Invalid Email"} + errNoCookie = errorResponse{status: http.StatusUnauthorized, message: "Cookie not found."} +) + +func handlerError(w http.ResponseWriter, err error) { + if err != nil { + if httpError, ok := err.(*errorResponse); ok { + http.Error(w, httpError.message, httpError.status) + return + } + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } +} diff --git a/api/internal/db/migration/schema.sql b/api/internal/db/migration/schema.sql new file mode 100644 index 00000000..f43b6c2a --- /dev/null +++ b/api/internal/db/migration/schema.sql @@ -0,0 +1,12 @@ +-- +migrate Up +CREATE TABLE + users ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + username text NOT NULL, + email text NOT NULL, + password text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL DEFAULT NULL, + UNIQUE (email) + ); diff --git a/api/internal/db/user/db.go b/api/internal/db/user/db.go new file mode 100644 index 00000000..25ef130e --- /dev/null +++ b/api/internal/db/user/db.go @@ -0,0 +1,138 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.14.0 + +package user + +import ( + "context" + "database/sql" + "fmt" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +func Prepare(ctx context.Context, db DBTX) (*Queries, error) { + q := Queries{db: db} + var err error + if q.createUserStmt, err = db.PrepareContext(ctx, createUser); err != nil { + return nil, fmt.Errorf("error preparing query CreateUser: %w", err) + } + if q.deleteUserStmt, err = db.PrepareContext(ctx, deleteUser); err != nil { + return nil, fmt.Errorf("error preparing query DeleteUser: %w", err) + } + if q.getUserByEmailStmt, err = db.PrepareContext(ctx, getUserByEmail); err != nil { + return nil, fmt.Errorf("error preparing query GetUserByEmail: %w", err) + } + if q.getUserByIDStmt, err = db.PrepareContext(ctx, getUserByID); err != nil { + return nil, fmt.Errorf("error preparing query GetUserByID: %w", err) + } + if q.listUsersStmt, err = db.PrepareContext(ctx, listUsers); err != nil { + return nil, fmt.Errorf("error preparing query ListUsers: %w", err) + } + if q.updateUserStmt, err = db.PrepareContext(ctx, updateUser); err != nil { + return nil, fmt.Errorf("error preparing query UpdateUser: %w", err) + } + return &q, nil +} + +func (q *Queries) Close() error { + var err error + if q.createUserStmt != nil { + if cerr := q.createUserStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createUserStmt: %w", cerr) + } + } + if q.deleteUserStmt != nil { + if cerr := q.deleteUserStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteUserStmt: %w", cerr) + } + } + if q.getUserByEmailStmt != nil { + if cerr := q.getUserByEmailStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getUserByEmailStmt: %w", cerr) + } + } + if q.getUserByIDStmt != nil { + if cerr := q.getUserByIDStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getUserByIDStmt: %w", cerr) + } + } + if q.listUsersStmt != nil { + if cerr := q.listUsersStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listUsersStmt: %w", cerr) + } + } + if q.updateUserStmt != nil { + if cerr := q.updateUserStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateUserStmt: %w", cerr) + } + } + return err +} + +func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (sql.Result, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...) + case stmt != nil: + return stmt.ExecContext(ctx, args...) + default: + return q.db.ExecContext(ctx, query, args...) + } +} + +func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...) + case stmt != nil: + return stmt.QueryContext(ctx, args...) + default: + return q.db.QueryContext(ctx, query, args...) + } +} + +func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) *sql.Row { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...) + case stmt != nil: + return stmt.QueryRowContext(ctx, args...) + default: + return q.db.QueryRowContext(ctx, query, args...) + } +} + +type Queries struct { + db DBTX + tx *sql.Tx + createUserStmt *sql.Stmt + deleteUserStmt *sql.Stmt + getUserByEmailStmt *sql.Stmt + getUserByIDStmt *sql.Stmt + listUsersStmt *sql.Stmt + updateUserStmt *sql.Stmt +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + tx: tx, + createUserStmt: q.createUserStmt, + deleteUserStmt: q.deleteUserStmt, + getUserByEmailStmt: q.getUserByEmailStmt, + getUserByIDStmt: q.getUserByIDStmt, + listUsersStmt: q.listUsersStmt, + updateUserStmt: q.updateUserStmt, + } +} diff --git a/api/internal/db/user/models.go b/api/internal/db/user/models.go new file mode 100644 index 00000000..bf03bb60 --- /dev/null +++ b/api/internal/db/user/models.go @@ -0,0 +1,20 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.14.0 + +package user + +import ( + "database/sql" + "time" +) + +type User struct { + ID int64 `db:"id" json:"id"` + Username string `db:"username" json:"username"` + Email string `db:"email" json:"email"` + Password string `db:"password" json:"password"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` + DeletedAt sql.NullTime `db:"deleted_at" json:"deleted_at"` +} diff --git a/api/internal/db/user/query.sql b/api/internal/db/user/query.sql new file mode 100644 index 00000000..a6219cca --- /dev/null +++ b/api/internal/db/user/query.sql @@ -0,0 +1,54 @@ +-- name: GetUserByID :one +SELECT + * +FROM + users +WHERE + id = ? +LIMIT + 1; + +-- name: GetUserByEmail :one +SELECT + * +FROM + users +WHERE + email = ? +LIMIT + 1; + +-- name: ListUsers :many +SELECT + * +FROM + users +ORDER BY + id; + +-- name: CreateUser :execresult +INSERT INTO + users ( + username, + email, + password, + created_at, + updated_at, + deleted_at + ) +VALUES + (?, ?, ?, ?, ?, ?); + +-- name: UpdateUser :execresult +UPDATE + users +SET + username = ? +WHERE + id = ?; + +-- name: DeleteUser :exec +DELETE FROM + users +WHERE + id = ?; diff --git a/api/internal/db/user/query.sql.go b/api/internal/db/user/query.sql.go new file mode 100644 index 00000000..f2ee335f --- /dev/null +++ b/api/internal/db/user/query.sql.go @@ -0,0 +1,168 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.14.0 +// source: query.sql + +package user + +import ( + "context" + "database/sql" + "time" +) + +const createUser = `-- name: CreateUser :execresult +INSERT INTO + users ( + username, + email, + password, + created_at, + updated_at, + deleted_at + ) +VALUES + (?, ?, ?, ?, ?, ?) +` + +type CreateUserParams struct { + Username string `db:"username" json:"username"` + Email string `db:"email" json:"email"` + Password string `db:"password" json:"password"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` + DeletedAt sql.NullTime `db:"deleted_at" json:"deleted_at"` +} + +func (q *Queries) CreateUser(ctx context.Context, arg *CreateUserParams) (sql.Result, error) { + return q.exec(ctx, q.createUserStmt, createUser, + arg.Username, + arg.Email, + arg.Password, + arg.CreatedAt, + arg.UpdatedAt, + arg.DeletedAt, + ) +} + +const deleteUser = `-- name: DeleteUser :exec +DELETE FROM + users +WHERE + id = ? +` + +func (q *Queries) DeleteUser(ctx context.Context, id int64) error { + _, err := q.exec(ctx, q.deleteUserStmt, deleteUser, id) + return err +} + +const getUserByEmail = `-- name: GetUserByEmail :one +SELECT + id, username, email, password, created_at, updated_at, deleted_at +FROM + users +WHERE + email = ? +LIMIT + 1 +` + +func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { + row := q.queryRow(ctx, q.getUserByEmailStmt, getUserByEmail, email) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.Password, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} + +const getUserByID = `-- name: GetUserByID :one +SELECT + id, username, email, password, created_at, updated_at, deleted_at +FROM + users +WHERE + id = ? +LIMIT + 1 +` + +func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) { + row := q.queryRow(ctx, q.getUserByIDStmt, getUserByID, id) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.Password, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} + +const listUsers = `-- name: ListUsers :many +SELECT + id, username, email, password, created_at, updated_at, deleted_at +FROM + users +ORDER BY + id +` + +func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { + rows, err := q.query(ctx, q.listUsersStmt, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + items := []User{} + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.Password, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateUser = `-- name: UpdateUser :execresult +UPDATE + users +SET + username = ? +WHERE + id = ? +` + +type UpdateUserParams struct { + Username string `db:"username" json:"username"` + ID int64 `db:"id" json:"id"` +} + +func (q *Queries) UpdateUser(ctx context.Context, arg *UpdateUserParams) (sql.Result, error) { + return q.exec(ctx, q.updateUserStmt, updateUser, arg.Username, arg.ID) +} diff --git a/api/internal/db/user/schema.sql b/api/internal/db/user/schema.sql new file mode 100644 index 00000000..1a447471 --- /dev/null +++ b/api/internal/db/user/schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE + users ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + username text NOT NULL, + email text NOT NULL, + password text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL DEFAULT NULL, + UNIQUE (email) + ); diff --git a/api/jam.go b/api/jam.go new file mode 100644 index 00000000..498524eb --- /dev/null +++ b/api/jam.go @@ -0,0 +1,171 @@ +package api + +import ( + "context" + "database/sql" + "encoding/json" + "io" + "log" + "net/http" + "sync" + + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" + "github.com/google/uuid" + "github.com/rog-golang-buddies/rapidmidiex/api/internal/db/user" +) + +type jamConn struct { + mu sync.Mutex + conn io.ReadWriter + + user.User +} + +type jamSession struct { + mu sync.RWMutex + conns map[string]*jamConn + out chan []interface{} + + id string + name string + tempo uint +} + +type JamService struct { + DBCon *sql.DB + mu sync.RWMutex + sessions map[string]*jamSession +} + +type ( + newSessionReq struct { + Name string `json:"name"` + Tempo uint `json:"tempo"` + } + + joinSessionReq struct { + SessionID string `json:"session_id"` + } +) + +func (s *JamService) NewSession(w http.ResponseWriter, r *http.Request) { + si := newSessionReq{} + if err := parse(r, &si); err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + session := jamSession{ + conns: make(map[string]*jamConn), + out: make(chan []interface{}), + + id: uuid.NewString(), + name: si.Name, + tempo: si.Tempo, + } + s.addSession(&session) + + w.WriteHeader(http.StatusOK) +} + +func (s *JamService) JoinSession(w http.ResponseWriter, r *http.Request) { + email, ok := r.Context().Value(emailCtxKey).(string) + if !ok { + w.WriteHeader(http.StatusBadRequest) // TODO: shouldn't respond with bad request. + return + } + + ji := joinSessionReq{} + if err := parse(r, &ji); err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + session := s.sessions[ji.SessionID] + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + c := jamConn{} + q := user.New(s.DBCon) + userInfo, err := q.GetUserByEmail(context.Background(), email) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + c.User = userInfo + c.conn = conn + session.addConn(&c) + session.broadcast("Welcome " + userInfo.Username + "!") + + w.WriteHeader(http.StatusOK) +} + +func (s *JamService) addSession(js *jamSession) { + s.mu.Lock() + { + s.sessions[js.id] = js + } + s.mu.Unlock() +} + +func (s *jamSession) addConn(jc *jamConn) { + s.mu.Lock() + { + s.conns[jc.Email] = jc + } + s.mu.Unlock() +} + +func (c *jamConn) write(i interface{}) error { + w := wsutil.NewWriter(c.conn, ws.StateServerSide, ws.OpText) + encoder := json.NewEncoder(w) + + c.mu.Lock() + defer c.mu.Unlock() + + if err := encoder.Encode(i); err != nil { + return err + } + + return w.Flush() +} + +func (c *jamConn) writeRaw(b []byte) error { + c.mu.Lock() + defer c.mu.Unlock() + + _, err := c.conn.Write(b) + return err +} + +// func (s *jamSession) writer(i interface{}) { +// for bts := range c.out { +// s.mu.RLock() +// cs := s.conns +// s.mu.RUnlock() +// +// for _, c := range cs { +// c := c // For closure. +// c.writeRaw(bts) +// } +// } +// } + +func (s *jamSession) broadcast(i interface{}) { + for _, c := range s.conns { + select { + case <-s.out: + c.write(i) + default: + delete(s.conns, c.Email) + // c.close() + } + } +} diff --git a/api/middlewares.go b/api/middlewares.go new file mode 100644 index 00000000..0ccdce61 --- /dev/null +++ b/api/middlewares.go @@ -0,0 +1,37 @@ +package api + +import ( + "context" + "net/http" + "strings" +) + +type ctxKey string + +var emailCtxKey = ctxKey("email") + +func (s *AuthService) CheckAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + at := r.Header.Get(authorizationHeader) + bearer := strings.Split(at, " ") + if !(len(bearer) > 1) { + w.WriteHeader(http.StatusUnauthorized) + return + } + userInfo, err := parseAccessTokenWithValidate(bearer[1]) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + privateClaims := userInfo.PrivateClaims() + email, ok := privateClaims["email"].(string) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + + ctx := context.WithValue(r.Context(), emailCtxKey, email) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/api/server.go b/api/server.go new file mode 100644 index 00000000..24abbc38 --- /dev/null +++ b/api/server.go @@ -0,0 +1,66 @@ +package api + +import ( + "context" + "log" + "net" + "net/http" + "os/signal" + "syscall" + "time" + + "github.com/go-chi/chi/v5" + "github.com/rs/cors" + "golang.org/x/sync/errgroup" +) + +type Server struct { + Host string + Port string + Router *chi.Mux +} + +func (s *Server) ServeHTTP() { + // "*" shouldn't be used as AllowedOrigins + c := cors.Options{ + AllowedOrigins: []string{"http://localhost:3000", "http://127.0.0.1:3000"}, + AllowCredentials: true, + AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodPatch}, + AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + } + h := cors.New(c).Handler(s.Router) + + serverCtx, serverStop := signal.NotifyContext( + context.Background(), + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + defer serverStop() + + server := http.Server{ + Addr: s.Port, + Handler: h, + ReadTimeout: 10 * time.Second, // max time to read request from the client + WriteTimeout: 10 * time.Second, // max time to write response to the client + IdleTimeout: 120 * time.Second, // max time for connections using TCP Keep-Alive + BaseContext: func(_ net.Listener) context.Context { + return serverCtx + }, + } + + g, gCtx := errgroup.WithContext(serverCtx) + g.Go(func() error { + // Run the server + return server.ListenAndServe() + }) + g.Go(func() error { + <-gCtx.Done() + return server.Shutdown(context.Background()) + }) + + if err := g.Wait(); err != nil { + log.Printf("exit reason: %s \n", err) + } +} diff --git a/api/utils.go b/api/utils.go new file mode 100644 index 00000000..b12df655 --- /dev/null +++ b/api/utils.go @@ -0,0 +1,31 @@ +package api + +import ( + "encoding/json" + "net/http" + "strings" +) + +func isEmptyString(s string) bool { + return len(strings.TrimSpace(s)) == 0 +} + +func parse(r *http.Request, out interface{}) error { + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&out) + if err != nil { + return err + } + + return nil +} + +type errorResponse struct { + status int + message string +} + +// custom error type for detecting internal application errors +func (e *errorResponse) Error() string { + return e.message +} diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/client/README.md b/client/README.md new file mode 100644 index 00000000..4ef762ff --- /dev/null +++ b/client/README.md @@ -0,0 +1,48 @@ +# Svelte + TS + Vite + +This template should help get you started developing with Svelte and TypeScript in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). + +## Need an official Svelte framework? + +Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. + +## Technical considerations + +**Why use this over SvelteKit?** + +- It brings its own routing solution which might not be preferable for some users. +- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. + `vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example. + +This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. + +Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. + +**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** + +Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. + +**Why include `.vscode/extensions.json`?** + +Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. + +**Why enable `allowJs` in the TS template?** + +While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. + +**Why is HMR not preserving my local component state?** + +HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). + +If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. + +```ts +// store.ts +// An extremely simple external store +import { writable } from 'svelte/store' +export default writable(0) +``` diff --git a/client/index.html b/client/index.html new file mode 100644 index 00000000..b5b12526 --- /dev/null +++ b/client/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Svelte + TS + + +
+ + + diff --git a/client/package.json b/client/package.json new file mode 100644 index 00000000..e9ba764d --- /dev/null +++ b/client/package.json @@ -0,0 +1,23 @@ +{ + "name": "client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^1.0.1", + "@tsconfig/svelte": "^3.0.0", + "sass": "^1.54.8", + "svelte": "^3.49.0", + "svelte-check": "^2.8.0", + "svelte-preprocess": "^4.10.7", + "tslib": "^2.4.0", + "typescript": "^4.6.4", + "vite": "^3.0.7" + } +} diff --git a/client/public/vite.svg b/client/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/App.svelte b/client/src/App.svelte new file mode 100644 index 00000000..770a5afc --- /dev/null +++ b/client/src/App.svelte @@ -0,0 +1,9 @@ + + +
+ + diff --git a/client/src/assets/svelte.svg b/client/src/assets/svelte.svg new file mode 100644 index 00000000..c5e08481 --- /dev/null +++ b/client/src/assets/svelte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/main.ts b/client/src/main.ts new file mode 100644 index 00000000..5c1f795f --- /dev/null +++ b/client/src/main.ts @@ -0,0 +1,8 @@ +import './app.css' +import App from './App.svelte' + +const app = new App({ + target: document.getElementById('app') +}) + +export default app diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts new file mode 100644 index 00000000..4078e747 --- /dev/null +++ b/client/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/client/svelte.config.js b/client/svelte.config.js new file mode 100644 index 00000000..3630bb39 --- /dev/null +++ b/client/svelte.config.js @@ -0,0 +1,7 @@ +import sveltePreprocess from 'svelte-preprocess' + +export default { + // Consult https://github.com/sveltejs/svelte-preprocess + // for more information about preprocessors + preprocess: sveltePreprocess() +} diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 00000000..d3830319 --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + "baseUrl": ".", + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": true, + "checkJs": true, + "isolatedModules": true + }, + "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json new file mode 100644 index 00000000..65dbdb96 --- /dev/null +++ b/client/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node" + }, + "include": ["vite.config.ts"] +} diff --git a/client/vite.config.ts b/client/vite.config.ts new file mode 100644 index 00000000..401b4d4b --- /dev/null +++ b/client/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [svelte()] +}) diff --git a/client/yarn.lock b/client/yarn.lock new file mode 100644 index 00000000..0a2e328b --- /dev/null +++ b/client/yarn.lock @@ -0,0 +1,733 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@esbuild/linux-loong64@0.14.54": + version "0.14.54" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028" + integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw== + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.15" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" + integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@rollup/pluginutils@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" + integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + +"@sveltejs/vite-plugin-svelte@^1.0.1": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.4.tgz#363a0adeb9221c35abb65197c6db0754b9994a08" + integrity sha512-UZco2fdj0OVuRWC0SUJjEOftITc2IeHLFJNp00ym9MuQ9dShnlO4P29G8KUxRlcS7kSpzHuko6eCR9MOALj7lQ== + dependencies: + "@rollup/pluginutils" "^4.2.1" + debug "^4.3.4" + deepmerge "^4.2.2" + kleur "^4.1.5" + magic-string "^0.26.2" + svelte-hmr "^0.14.12" + +"@tsconfig/svelte@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@tsconfig/svelte/-/svelte-3.0.0.tgz#b06e059209f04c414de0069f2f0e2796d979fc6f" + integrity sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg== + +"@types/node@*": + version "18.7.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.14.tgz#0fe081752a3333392d00586d815485a17c2cf3c9" + integrity sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA== + +"@types/pug@^2.0.4": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6" + integrity sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg== + +"@types/sass@^1.16.0": + version "1.43.1" + resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.43.1.tgz#86bb0168e9e881d7dade6eba16c9ed6d25dc2f68" + integrity sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g== + dependencies: + "@types/node" "*" + +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +buffer-crc32@^0.2.5: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.1: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +detect-indent@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" + integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== + +es6-promise@^3.1.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + integrity sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg== + +esbuild-android-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be" + integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ== + +esbuild-android-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771" + integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg== + +esbuild-darwin-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25" + integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug== + +esbuild-darwin-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73" + integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw== + +esbuild-freebsd-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d" + integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg== + +esbuild-freebsd-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48" + integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q== + +esbuild-linux-32@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5" + integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw== + +esbuild-linux-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652" + integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg== + +esbuild-linux-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b" + integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig== + +esbuild-linux-arm@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59" + integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw== + +esbuild-linux-mips64le@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34" + integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw== + +esbuild-linux-ppc64le@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e" + integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ== + +esbuild-linux-riscv64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8" + integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg== + +esbuild-linux-s390x@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6" + integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA== + +esbuild-netbsd-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81" + integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w== + +esbuild-openbsd-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b" + integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw== + +esbuild-sunos-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da" + integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw== + +esbuild-windows-32@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31" + integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w== + +esbuild-windows-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4" + integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ== + +esbuild-windows-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982" + integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg== + +esbuild@^0.14.47: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2" + integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA== + optionalDependencies: + "@esbuild/linux-loong64" "0.14.54" + esbuild-android-64 "0.14.54" + esbuild-android-arm64 "0.14.54" + esbuild-darwin-64 "0.14.54" + esbuild-darwin-arm64 "0.14.54" + esbuild-freebsd-64 "0.14.54" + esbuild-freebsd-arm64 "0.14.54" + esbuild-linux-32 "0.14.54" + esbuild-linux-64 "0.14.54" + esbuild-linux-arm "0.14.54" + esbuild-linux-arm64 "0.14.54" + esbuild-linux-mips64le "0.14.54" + esbuild-linux-ppc64le "0.14.54" + esbuild-linux-riscv64 "0.14.54" + esbuild-linux-s390x "0.14.54" + esbuild-netbsd-64 "0.14.54" + esbuild-openbsd-64 "0.14.54" + esbuild-sunos-64 "0.14.54" + esbuild-windows-32 "0.14.54" + esbuild-windows-64 "0.14.54" + esbuild-windows-arm64 "0.14.54" + +estree-walker@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +fast-glob@^3.2.7: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.1.3: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +immutable@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef" + integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.9.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" + integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== + dependencies: + has "^1.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +kleur@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" + integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== + +magic-string@^0.25.7: + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" + +magic-string@^0.26.2: + version "0.26.3" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.3.tgz#25840b875140f7b4785ab06bddc384270b7dd452" + integrity sha512-u1Po0NDyFcwdg2nzHT88wSK0+Rih0N1M+Ph1Sp08k8yvFFU3KR72wryS7e1qMPJypt99WB7fIFVCA92mQrMjrg== + dependencies: + sourcemap-codec "^1.4.8" + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +mkdirp@^0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nanoid@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +postcss@^8.4.16: + version "8.4.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c" + integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.22.1: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^2.5.2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +"rollup@>=2.75.6 <2.77.0 || ~2.77.0": + version "2.77.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.3.tgz#8f00418d3a2740036e15deb653bed1a90ee0cc12" + integrity sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g== + optionalDependencies: + fsevents "~2.3.2" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +sade@^1.7.4: + version "1.8.1" + resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" + integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== + dependencies: + mri "^1.1.0" + +sander@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/sander/-/sander-0.5.1.tgz#741e245e231f07cafb6fdf0f133adfa216a502ad" + integrity sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA== + dependencies: + es6-promise "^3.1.2" + graceful-fs "^4.1.3" + mkdirp "^0.5.1" + rimraf "^2.5.2" + +sass@^1.54.8: + version "1.54.8" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.8.tgz#4adef0dd86ea2b1e4074f551eeda4fc5f812a996" + integrity sha512-ib4JhLRRgbg6QVy6bsv5uJxnJMTS2soVcCp9Y88Extyy13A8vV0G1fAwujOzmNkFQbR3LvedudAMbtuNRPbQww== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +sorcery@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.10.0.tgz#8ae90ad7d7cb05fc59f1ab0c637845d5c15a52b7" + integrity sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g== + dependencies: + buffer-crc32 "^0.2.5" + minimist "^1.2.0" + sander "^0.5.0" + sourcemap-codec "^1.3.0" + +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +sourcemap-codec@^1.3.0, sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svelte-check@^2.8.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-2.9.0.tgz#94a4bbe5bccade5a9edae987f417040ab7af4a2a" + integrity sha512-9AVrtP7WbfDgCdqTZNPdj5CCCy1OrYMxFVWAWzNw7fl93c9klFJFtqzVXa6fovfQ050CcpUyJE2dPFL9TFAREw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.9" + chokidar "^3.4.1" + fast-glob "^3.2.7" + import-fresh "^3.2.1" + picocolors "^1.0.0" + sade "^1.7.4" + svelte-preprocess "^4.0.0" + typescript "*" + +svelte-hmr@^0.14.12: + version "0.14.12" + resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.14.12.tgz#a127aec02f1896500b10148b2d4d21ddde39973f" + integrity sha512-4QSW/VvXuqVcFZ+RhxiR8/newmwOCTlbYIezvkeN6302YFRE8cXy0naamHcjz8Y9Ce3ITTZtrHrIL0AGfyo61w== + +svelte-preprocess@^4.0.0, svelte-preprocess@^4.10.7: + version "4.10.7" + resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-4.10.7.tgz#3626de472f51ffe20c9bc71eff5a3da66797c362" + integrity sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw== + dependencies: + "@types/pug" "^2.0.4" + "@types/sass" "^1.16.0" + detect-indent "^6.0.0" + magic-string "^0.25.7" + sorcery "^0.10.0" + strip-indent "^3.0.0" + +svelte@^3.49.0: + version "3.50.0" + resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.50.0.tgz#d11a7a6bd1e084ec051d55104a9af8bccf54461f" + integrity sha512-zXeOUDS7+85i+RxLN+0iB6PMbGH7OhEgjETcD1fD8ZrhuhNFxYxYEHU41xuhkHIulJavcu3PKbPyuCrBxdxskQ== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tslib@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + +typescript@*, typescript@^4.6.4: + version "4.8.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790" + integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw== + +vite@^3.0.7: + version "3.0.9" + resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.9.tgz#45fac22c2a5290a970f23d66c1aef56a04be8a30" + integrity sha512-waYABTM+G6DBTCpYAxvevpG50UOlZuynR0ckTK5PawNVt7ebX6X7wNXHaGIO6wYYFXSM7/WcuFuO2QzhBB6aMw== + dependencies: + esbuild "^0.14.47" + postcss "^8.4.16" + resolve "^1.22.1" + rollup ">=2.75.6 <2.77.0 || ~2.77.0" + optionalDependencies: + fsevents "~2.3.2" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index 63e4dc31..00000000 --- a/cmd/main.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -import "fmt" - -func main() { - // Feel free to delete this file. - fmt.Println("Hello Gophers") -} diff --git a/cmd/server/main.go b/cmd/server/main.go index 63e4dc31..7eea3675 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,8 +1,101 @@ package main -import "fmt" +import ( + "database/sql" + "fmt" + "log" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-redis/redis/v9" + "github.com/rog-golang-buddies/rapidmidiex" + "github.com/rog-golang-buddies/rapidmidiex/api" + migrate "github.com/rubenv/sql-migrate" + "github.com/spf13/viper" + + _ "github.com/go-sql-driver/mysql" +) func main() { - // Feel free to delete this file. - fmt.Println("Hello Gophers") + err := rmx.LoadConfig() + if err != nil { + log.Fatalf("failed to read config: %v", err.Error()) + } + + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true", + viper.GetString("DB_USER"), + viper.GetString("DB_PASSWORD"), + viper.GetString("DB_HOST"), + viper.GetString("DB_PORT"), + viper.GetString("DB_NAME"), + ) + + db, err := sql.Open("mysql", dsn) + if err != nil { + log.Fatalln(err) + } + + // run db migrations + migrations := &migrate.FileMigrationSource{ + Dir: "api/internal/db/migration", + } + + _, err = migrate.Exec(db, "mysql", migrations, migrate.Up) + if err != nil { + log.Fatalln(err) + } + + rtdb := redis.NewClient(&redis.Options{ + Addr: viper.GetString("REDIS_ADDR"), + Password: viper.GetString("REDIS_PASSWORD"), + DB: 0, + }) + cidb := redis.NewClient(&redis.Options{ + Addr: viper.GetString("REDIS_ADDR"), + Password: viper.GetString("REDIS_PASSWORD"), + DB: 1, + }) + ptdb := redis.NewClient(&redis.Options{ + Addr: viper.GetString("REDIS_ADDR"), + Password: viper.GetString("REDIS_PASSWORD"), + DB: 2, + }) + + authService := api.AuthService{ + DBCon: db, + RedisRefreshTokenDB: rtdb, + RedisClientIDDB: cidb, + RedisPasswordTokenDB: ptdb, + } + jamService := api.JamService{ + DBCon: db, + } + + server := api.Server{ + Port: ":8080", + Router: chi.NewMux(), + } + + server.Router.Route("/api/v1", func(r chi.Router) { + r.Use(middleware.Logger) + r.Route("/auth", func(r chi.Router) { + r.Post("/register", authService.Register) + r.Post("/login", authService.Login) + r.Get("/refresh_token", authService.RefreshToken) + r.Get("/logout", authService.Logout) + }) + r.Route("/jam", func(r chi.Router) { + r.Use(authService.CheckAuth) + r.Post("/new", jamService.NewSession) + r.Get("/{session_id}/join", jamService.JoinSession) + }) + r.Route("/users", func(r chi.Router) { + r.Use(authService.CheckAuth) + r.Get("/me", authService.GetUserInfo) + r.Patch("/me", authService.UpdateUserInfo) + }) + }) + + log.Println("starting the server") + server.ServeHTTP() } diff --git a/config.go b/config.go new file mode 100644 index 00000000..7bc1064f --- /dev/null +++ b/config.go @@ -0,0 +1,18 @@ +package rmx + +import "github.com/spf13/viper" + +func LoadConfig() error { + viper.SetConfigName("config") // name of config file (without extension) + viper.SetConfigType("env") // REQUIRED if the config file does not have the extension in the name + viper.AddConfigPath(".") // optionally look for config in the working directory + + viper.AutomaticEnv() + + err := viper.ReadInConfig() // Find and read the config file + if err != nil { // Handle errors reading the config file + return err + } + + return nil +} diff --git a/go.mod b/go.mod index 99b78ec0..0999dd46 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,49 @@ module github.com/rog-golang-buddies/rapidmidiex go 1.18 + +require ( + github.com/go-chi/chi/v5 v5.0.7 + github.com/go-redis/redis/v9 v9.0.0-beta.2 + github.com/go-sql-driver/mysql v1.6.0 + github.com/gobwas/ws v1.1.0 + github.com/google/uuid v1.3.0 + github.com/lestrrat-go/jwx/v2 v2.0.6 + github.com/pmoieni/nimbus-cloud v0.0.0-20220826211613-3fdd55558ccb + github.com/rs/cors v1.8.2 + github.com/rubenv/sql-migrate v1.1.2 + github.com/spf13/viper v1.12.0 + golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 + golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde +) + +require ( + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/go-gorp/gorp/v3 v3.0.2 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/goccy/go-json v0.9.11 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/lestrrat-go/blackmagic v1.0.1 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.4 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.0 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.1 // indirect + github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.3.0 // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/text v0.3.7 // indirect + gopkg.in/ini.v1 v1.66.4 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..5af95138 --- /dev/null +++ b/go.sum @@ -0,0 +1,738 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +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.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gorp/gorp/v3 v3.0.2 h1:ULqJXIekoqMx29FI5ekXXFoH1dT2Vc8UhnRzBg+Emz4= +github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-redis/redis/v9 v9.0.0-beta.2 h1:ZSr84TsnQyKMAg8gnV+oawuQezeJR11/09THcWCQzr4= +github.com/go-redis/redis/v9 v9.0.0-beta.2/go.mod h1:Bldcd/M/bm9HbnNPi/LUtYBSD8ttcZYBMupwMXhdU0o= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= +github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= +github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= +github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= +github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= +github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= +github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= +github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= +github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= +github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= +github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.0.6 h1:RlyYNLV892Ed7+FTfj1ROoF6x7WxL965PGTHso/60G0= +github.com/lestrrat-go/jwx/v2 v2.0.6/go.mod h1:aVrGuwEr3cp2Prw6TtQvr8sQxe+84gruID5C9TxT64Q= +github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= +github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= +github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= +github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= +github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmoieni/nimbus-cloud v0.0.0-20220826211613-3fdd55558ccb h1:Kek4TR8il3xlCQfbY6yELvpmwhj6heS4Gn+GsSSqy2Y= +github.com/pmoieni/nimbus-cloud v0.0.0-20220826211613-3fdd55558ccb/go.mod h1:cp4EDSZDp5QDVIgH52Ibn/iZ+MGIddrpDqMzBXPSsy4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1 h1:oL4IBbcqwhhNWh31bjOX8C/OCy0zs9906d/VUru+bqg= +github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= +github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rubenv/sql-migrate v1.1.2 h1:9M6oj4e//owVVHYrFISmY9LBRw6gzkCNmD9MV36tZeQ= +github.com/rubenv/sql-migrate v1.1.2/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= +github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= +gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 00000000..540906dc --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,15 @@ +version: "2" +sql: + - schema: "api/internal/db/user/schema.sql" + queries: "api/internal/db/user/query.sql" + engine: "mysql" + gen: + go: + package: "user" + out: "api/internal/db/user" + emit_db_tags: true + emit_prepared_queries: true + emit_empty_slices: true + emit_params_struct_pointers: true + emit_json_tags: true + json_tags_case_style: "snake" From a5de7d68e9aecdeeebd861c04a9318d7b53abfc8 Mon Sep 17 00:00:00 2001 From: Parham Moieni Date: Thu, 8 Sep 2022 21:23:07 +0430 Subject: [PATCH 002/246] basic server scaffold without auth --- api/auth.go | 652 --------------------------- api/errors.go | 8 +- api/internal/db/migration/schema.sql | 12 - api/internal/db/user/db.go | 138 ------ api/internal/db/user/models.go | 20 - api/internal/db/user/query.sql | 54 --- api/internal/db/user/query.sql.go | 168 ------- api/internal/db/user/schema.sql | 11 - api/jam.go | 106 +++-- api/middlewares.go | 37 -- 10 files changed, 77 insertions(+), 1129 deletions(-) delete mode 100644 api/auth.go delete mode 100644 api/internal/db/migration/schema.sql delete mode 100644 api/internal/db/user/db.go delete mode 100644 api/internal/db/user/models.go delete mode 100644 api/internal/db/user/query.sql delete mode 100644 api/internal/db/user/query.sql.go delete mode 100644 api/internal/db/user/schema.sql delete mode 100644 api/middlewares.go diff --git a/api/auth.go b/api/auth.go deleted file mode 100644 index 2797977d..00000000 --- a/api/auth.go +++ /dev/null @@ -1,652 +0,0 @@ -package api - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "log" - "net/http" - "net/mail" - "time" - "unicode" - - "github.com/go-redis/redis/v9" - "github.com/go-sql-driver/mysql" - "github.com/google/uuid" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/rog-golang-buddies/rapidmidiex/api/internal/db/user" - "github.com/spf13/viper" - "golang.org/x/crypto/bcrypt" -) - -type AuthService struct { - DBCon *sql.DB - RedisRefreshTokenDB *redis.Client - RedisClientIDDB *redis.Client - RedisPasswordTokenDB *redis.Client -} - -const ( - authorizationHeader = "Authorization" - refreshTokenCookieName = "RMX_DIRECT_RT" - refreshTokenCookiePath = "/api/v1" -) - -type loginRes struct { - IDToken string `json:"id_token"` - AccessToken string `json:"access_token"` -} - -type refreshTokenRes struct { - AccessToken string `json:"access_token"` -} - -type userInfoRes struct { - Username string `json:"username"` - Email string `json:"email"` -} - -func (s *AuthService) Login(w http.ResponseWriter, r *http.Request) { - // get user credentials from request and bind it to UserLoginCreds type - user := user.User{} - if err := parse(r, &user); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - // Login the user with provided credentials - tokens, err := s.login(&user) - handlerError(w, err) - - rtCookie := http.Cookie{ - Path: refreshTokenCookiePath, - Name: refreshTokenCookieName, - Value: tokens.refreshToken, - HttpOnly: true, - Secure: false, // set to true in production - SameSite: http.SameSiteLaxMode, - Expires: time.Now().UTC().Add(refreshTokenExpiry), - } - - res := loginRes{ - IDToken: tokens.idToken, - AccessToken: tokens.accessToken, - } - - http.SetCookie(w, &rtCookie) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(res) - if err != nil { - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -func (s *AuthService) Register(w http.ResponseWriter, r *http.Request) { - user := user.User{} - if err := parse(r, &user); err != nil { - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - err := s.register(&user) - handlerError(w, err) - - w.WriteHeader(http.StatusOK) -} - -func (s *AuthService) RefreshToken(w http.ResponseWriter, r *http.Request) { - rtCookie, err := r.Cookie(refreshTokenCookieName) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - tokens, err := s.refreshToken(rtCookie.Value) - handlerError(w, err) - - newRTCookie := http.Cookie{ - Path: refreshTokenCookiePath, - Name: refreshTokenCookieName, - Value: tokens.refreshToken, - HttpOnly: true, - Secure: false, // set to true in production - SameSite: http.SameSiteLaxMode, - Expires: time.Now().UTC().Add(refreshTokenExpiry), - } - res := refreshTokenRes{ - AccessToken: tokens.accessToken, - } - - http.SetCookie(w, &newRTCookie) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(res) - if err != nil { - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -func (s *AuthService) Logout(w http.ResponseWriter, r *http.Request) { - // remove refresh token cookie - cookie := &http.Cookie{ - Path: refreshTokenCookiePath, - Name: refreshTokenCookieName, - Value: "", - HttpOnly: true, - Secure: false, // set to true in production - SameSite: http.SameSiteLaxMode, - Expires: time.Unix(0, 0), - } - - http.SetCookie(w, cookie) - w.WriteHeader(http.StatusOK) -} - -// func (s *AuthService) GetUsersEmailList(w http.ResponseWriter, r *http.Request) { -// q := auth.New(s.DBCon) -// users, err := q.ListUsers(context.Background()) -// if err != nil { -// log.Println(err) -// w.WriteHeader(http.StatusInternalServerError) -// return -// } - -// res := usersEmailListRes{} -// for _, user := range users { -// res.Users = append(res.Users, user.Email) -// } - -// w.Header().Set("Content-Type", "application/json") -// w.WriteHeader(http.StatusOK) -// err = json.NewEncoder(w).Encode(res) -// if err != nil { -// log.Println(err) -// w.WriteHeader(http.StatusInternalServerError) -// return -// } -// } - -func (s *AuthService) GetUserInfo(w http.ResponseWriter, r *http.Request) { - email, ok := r.Context().Value(emailCtxKey).(string) - if !ok { - w.WriteHeader(http.StatusUnauthorized) - return - } - - user, err := s.getUserInfo(email) - handlerError(w, err) - - res := userInfoRes{} - res.Username = user.Username - res.Email = user.Email - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - err = json.NewEncoder(w).Encode(res) - if err != nil { - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -func (s *AuthService) UpdateUserInfo(w http.ResponseWriter, r *http.Request) { - email, ok := r.Context().Value(emailCtxKey).(string) - if !ok { - w.WriteHeader(http.StatusUnauthorized) - return - } - - newUserInfo := user.User{} - if err := parse(r, &newUserInfo); err != nil { - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - err := s.updateUserInfo(email, &newUserInfo) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} - -// ------------------------ SERVICE ------------------------------- -type ( - authTokens struct { - idToken string - accessToken string - refreshToken string - } - refreshTokens struct { - accessToken string - refreshToken string - } - idTokenClaims struct { - Email string `json:"email"` - // emailVerified bool - } - accessTokenClaims struct { - Email string `json:"email"` - ClientID string `json:"client_id"` - } - refreshTokenClaims struct { - Email string `json:"email"` - ClientID string `json:"client_id"` - } -) - -const ( - idTokenExpiry = time.Hour * 10 - accessTokenExpiry = time.Minute * 5 - refreshTokenExpiry = time.Hour * 24 * 7 -) - -func (s *AuthService) register(u *user.User) error { - if isEmptyString(u.Username) { - return &errInvalidRegisterInfo - } - - err := validateEmail(u.Email) - if err != nil { - return err - } - - err = validatePassword(u.Password) - if err != nil { - return err - } - - userInfo := user.CreateUserParams{} - userInfo.Username = u.Username - userInfo.Email = u.Email - // u.EmailVerified = false // default value when user registers for the first time - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) - if err != nil { - return err - } - userInfo.Password = string(hashedPassword) - - ctx := context.Background() - q := user.New(s.DBCon) - _, err = q.CreateUser(ctx, &userInfo) - var mysqlErr *mysql.MySQLError - if err != nil { - if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { - return &errUserAlreadyExists - } - return err - } - - return nil -} - -func (s *AuthService) login(u *user.User) (authTokens, error) { - // check if provided email is valid - err := validateEmail(u.Email) - if err != nil { - return authTokens{}, err - } - - // get user info from database to create new authentication tokens - ctx := context.Background() - q := user.New(s.DBCon) - user, err := q.GetUserByEmail(ctx, u.Email) - if err != nil { - return authTokens{}, err - } - - // check user password - err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(u.Password)) - if err != nil { - // if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { - // err = &models.ErrorResponse{Status: http.StatusUnauthorized, Message: errWrongPassword} - // } - // if errors.Is(err, bcrypt.ErrHashTooShort) { - // err = &models.ErrorResponse{Status: http.StatusUnauthorized, Message: errWrongPassword} - // } - return authTokens{}, err - } - - clientID := uuid.New().String() - idClaims := idTokenClaims{ - Email: user.Email, - // emailVerified: u.EmailVerified, - } - it, err := genIDToken(&idClaims) - if err != nil { - return authTokens{}, err - } - - accessClaims := accessTokenClaims{ - Email: user.Email, - ClientID: clientID, - } - at, err := genAccessToken(&accessClaims) - if err != nil { - return authTokens{}, err - } - - refreshClaims := refreshTokenClaims{ - Email: user.Email, - ClientID: clientID, - } - rt, err := genRefreshToken(&refreshClaims) - if err != nil { - return authTokens{}, err - } - - tokens := authTokens{ - idToken: it, - accessToken: at, - refreshToken: rt, - } - return tokens, nil -} - -func (s *AuthService) refreshToken(rt string) (refreshTokens, error) { - // validate and parse the refresh token - rtPayload, err := parseRefreshTokenWithValidate(rt) - if err != nil { - return refreshTokens{}, err - } - - privateClaims := rtPayload.PrivateClaims() - - email, ok := privateClaims["email"].(string) - if !ok { - return refreshTokens{}, &errorResponse{status: http.StatusUnauthorized, message: http.StatusText(http.StatusUnauthorized)} - } - - // check for token reuse - reuse, err := s.isTokenUsed(rt) - if err != nil { - return refreshTokens{}, err - } - if reuse { - return refreshTokens{}, &errorResponse{status: http.StatusUnauthorized, message: http.StatusText(http.StatusUnauthorized)} - } - - // save used token in database to detect token reuse when this toke is used again - // since we have passed the token reuse check, this is the first time the token is being used - // so we save it in used refresh tokens list to detect future token reuses - err = s.saveRefreshToken(rt, email, rtPayload.Subject()) - if err != nil { - return refreshTokens{}, err - } - - // if the client id is revoked then the token is invalid and is reused by malicious user - revoked, err := s.isClientIDRevoked(rtPayload.Subject()) - if err != nil { - return refreshTokens{}, err - } - if revoked { - return refreshTokens{}, &errorResponse{status: http.StatusUnauthorized, message: http.StatusText(http.StatusUnauthorized)} - } - - // generate new access token from previous access token claims - newATClaims := accessTokenClaims{ - Email: email, - ClientID: rtPayload.Subject(), - } - newAT, err := genAccessToken(&newATClaims) - if err != nil { - return refreshTokens{}, err - } - - // generate new refresh token form previous access token claims - newRTClaims := refreshTokenClaims{ - Email: email, - ClientID: rtPayload.Subject(), - } - newRT, err := genRefreshToken(&newRTClaims) - if err != nil { - return refreshTokens{}, err - } - - tokens := refreshTokens{ - accessToken: newAT, - refreshToken: newRT, - } - - return tokens, nil -} - -func (s *AuthService) saveRefreshToken(token, email, clientID string) error { - t := refreshTokenClaims{ - Email: email, - ClientID: clientID, - } - b, err := json.Marshal(&t) - if err != nil { - return err - } - payload, err := parseRefreshToken(token) - if err != nil { - return err - } - d := time.Now().UTC().Sub(payload.IssuedAt()) - - _, err = s.RedisRefreshTokenDB.Set(context.Background(), token, string(b), d).Result() - if err != nil { - return err - } - - return nil -} - -func (s *AuthService) isTokenUsed(token string) (bool, error) { - // check if token is available in redis database - // if it's not then token is not reused - v, err := s.RedisRefreshTokenDB.Get(context.Background(), token).Result() - if err != nil { - switch err { - case redis.Nil: - return false, nil - default: - return false, err - } - } - - // token is available in redis database which means it's reused - // get token information containing client id and email of user - t := refreshTokenClaims{} - err = json.Unmarshal([]byte(v), &t) - if err != nil { - return false, err - } - - // save client id in redis database to deny any refresh token with the sub value of revoked client id - err = s.revokeClientID(t.ClientID, t.Email) - if err != nil { - return false, err - } - - return true, nil -} - -func (s *AuthService) revokeClientID(clientID, email string) error { - _, err := s.RedisClientIDDB.Set(context.Background(), clientID, email, refreshTokenExpiry).Result() - if err != nil { - return err - } - return nil -} - -func (s *AuthService) isClientIDRevoked(clientID string) (bool, error) { - // check if a key with client id exists - // if the key exists it means that the client id is revoked and token should be denied - // we don't need the email value here - _, err := s.RedisClientIDDB.Get(context.Background(), clientID).Result() - if err != nil { - switch err { - case redis.Nil: - return false, nil - default: - return false, err - } - } - - return true, nil -} - -func parseRefreshToken(token string) (payload jwt.Token, err error) { - payload, err = jwt.Parse([]byte(token), - jwt.WithKey(jwa.HS256, - []byte(viper.GetString("REFRESH_TOKEN_HMAC_SECRET")))) - return -} - -// checks access token validity and returns its payload -func parseAccessTokenWithValidate(token string) (payload jwt.Token, err error) { - payload, err = jwt.Parse([]byte(token), - jwt.WithKey(jwa.HS256, - []byte(viper.GetString("ACCESS_TOKEN_HMAC_SECRET"))), - jwt.WithValidate(true)) - return -} - -// checks refresh token validity and returns its payload -func parseRefreshTokenWithValidate(token string) (payload jwt.Token, err error) { - payload, err = jwt.Parse([]byte(token), - jwt.WithKey(jwa.HS256, - []byte(viper.GetString("REFRESH_TOKEN_HMAC_SECRET"))), - jwt.WithValidate(true)) - return -} - -func genIDToken(c *idTokenClaims) (string, error) { - token, err := jwt.NewBuilder(). - Issuer("http://localhost:8080"). - Subject(c.Email). - Audience([]string{"http://localhost:3000"}). - IssuedAt(time.Now().UTC()). - Expiration(time.Now().UTC().Add(idTokenExpiry)). - Claim("email", c.Email). - // Claim("email_verified", c.emailVerified). - Build() - if err != nil { - return "", err - } - - signed, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte(viper.GetString("ID_TOKEN_HMAC_SECRET")))) - if err != nil { - return "", err - } - - return string(signed), nil -} - -func genAccessToken(c *accessTokenClaims) (string, error) { - // scope := strings.Join(c.scope, " ") - token, err := jwt.NewBuilder(). - Issuer("http://localhost:8080"). - Subject(c.ClientID). - Audience([]string{"http://localhost:3000"}). - IssuedAt(time.Now().UTC()). - Expiration(time.Now().UTC().Add(accessTokenExpiry)). - Claim("email", c.Email). - Build() - if err != nil { - return "", err - } - - signed, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte(viper.GetString("ACCESS_TOKEN_HMAC_SECRET")))) - if err != nil { - return "", err - } - - return string(signed), nil -} - -func genRefreshToken(c *refreshTokenClaims) (string, error) { - token, err := jwt.NewBuilder(). - Issuer("http://localhost:8080"). - Subject(c.ClientID). - Audience([]string{"http://localhost:3000"}). - IssuedAt(time.Now().UTC()). - Expiration(time.Now().UTC().Add(refreshTokenExpiry)). - Claim("email", c.Email). - Build() - if err != nil { - return "", err - } - - signed, err := jwt.Sign(token, jwt.WithKey(jwa.HS256, []byte(viper.GetString("REFRESH_TOKEN_HMAC_SECRET")))) - if err != nil { - return "", err - } - - return string(signed), nil -} - -func validateEmail(e string) error { - if _, err := mail.ParseAddress(e); err != nil { - return &errInvalidEmail - } - return nil -} - -func validatePassword(p string) error { - var ( - hasMinLen = false - hasNumber = false - ) - if len(p) >= 8 { - hasMinLen = true - } - for _, char := range p { - if unicode.IsNumber(char) { - hasNumber = true - } - } - - if !(hasMinLen && hasNumber) { - return &errBadPassword - } - - return nil -} - -func (s *AuthService) getUserInfo(email string) (user.User, error) { - q := user.New(s.DBCon) - userInfo, err := q.GetUserByEmail(context.Background(), email) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - - } - return user.User{}, err - } - - return userInfo, nil -} - -func (s *AuthService) updateUserInfo(email string, u *user.User) error { - q := user.New(s.DBCon) - userInfo, err := q.GetUserByEmail(context.Background(), email) - if err != nil { - return err - } - - updateUser := user.UpdateUserParams{} - updateUser.ID = userInfo.ID - if isEmptyString(u.Username) { - return &errInvalidUpdateInfo - } - updateUser.Username = u.Username - - _, err = q.UpdateUser(context.Background(), &updateUser) - if err != nil { - return err - } - - return nil -} diff --git a/api/errors.go b/api/errors.go index 55694b4a..2eceee50 100644 --- a/api/errors.go +++ b/api/errors.go @@ -6,13 +6,9 @@ import ( ) var ( - errInvalidUpdateInfo = errorResponse{status: http.StatusBadRequest, message: "Invalid Username"} - errInvalidRegisterInfo = errorResponse{status: http.StatusBadRequest, message: "Invalid Register Info"} - errUserAlreadyExists = errorResponse{status: http.StatusForbidden, message: "User with the same email already exists."} - errUserNotFound = errorResponse{status: http.StatusNotFound, message: "User not found."} - errBadPassword = errorResponse{status: http.StatusBadRequest, message: "Password must contain at least 8 characters and one number"} - errInvalidEmail = errorResponse{status: http.StatusBadRequest, message: "Invalid Email"} errNoCookie = errorResponse{status: http.StatusUnauthorized, message: "Cookie not found."} + errUsernameAlreadyUsed = errorResponse{status: http.StatusForbidden, message: "This username is already used."} + errSessionNotFound = errorResponse{status: http.StatusNotFound, message: "Session not found."} ) func handlerError(w http.ResponseWriter, err error) { diff --git a/api/internal/db/migration/schema.sql b/api/internal/db/migration/schema.sql deleted file mode 100644 index f43b6c2a..00000000 --- a/api/internal/db/migration/schema.sql +++ /dev/null @@ -1,12 +0,0 @@ --- +migrate Up -CREATE TABLE - users ( - id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - username text NOT NULL, - email text NOT NULL, - password text NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - deleted_at TIMESTAMP NULL DEFAULT NULL, - UNIQUE (email) - ); diff --git a/api/internal/db/user/db.go b/api/internal/db/user/db.go deleted file mode 100644 index 25ef130e..00000000 --- a/api/internal/db/user/db.go +++ /dev/null @@ -1,138 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.14.0 - -package user - -import ( - "context" - "database/sql" - "fmt" -) - -type DBTX interface { - ExecContext(context.Context, string, ...interface{}) (sql.Result, error) - PrepareContext(context.Context, string) (*sql.Stmt, error) - QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) - QueryRowContext(context.Context, string, ...interface{}) *sql.Row -} - -func New(db DBTX) *Queries { - return &Queries{db: db} -} - -func Prepare(ctx context.Context, db DBTX) (*Queries, error) { - q := Queries{db: db} - var err error - if q.createUserStmt, err = db.PrepareContext(ctx, createUser); err != nil { - return nil, fmt.Errorf("error preparing query CreateUser: %w", err) - } - if q.deleteUserStmt, err = db.PrepareContext(ctx, deleteUser); err != nil { - return nil, fmt.Errorf("error preparing query DeleteUser: %w", err) - } - if q.getUserByEmailStmt, err = db.PrepareContext(ctx, getUserByEmail); err != nil { - return nil, fmt.Errorf("error preparing query GetUserByEmail: %w", err) - } - if q.getUserByIDStmt, err = db.PrepareContext(ctx, getUserByID); err != nil { - return nil, fmt.Errorf("error preparing query GetUserByID: %w", err) - } - if q.listUsersStmt, err = db.PrepareContext(ctx, listUsers); err != nil { - return nil, fmt.Errorf("error preparing query ListUsers: %w", err) - } - if q.updateUserStmt, err = db.PrepareContext(ctx, updateUser); err != nil { - return nil, fmt.Errorf("error preparing query UpdateUser: %w", err) - } - return &q, nil -} - -func (q *Queries) Close() error { - var err error - if q.createUserStmt != nil { - if cerr := q.createUserStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing createUserStmt: %w", cerr) - } - } - if q.deleteUserStmt != nil { - if cerr := q.deleteUserStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing deleteUserStmt: %w", cerr) - } - } - if q.getUserByEmailStmt != nil { - if cerr := q.getUserByEmailStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing getUserByEmailStmt: %w", cerr) - } - } - if q.getUserByIDStmt != nil { - if cerr := q.getUserByIDStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing getUserByIDStmt: %w", cerr) - } - } - if q.listUsersStmt != nil { - if cerr := q.listUsersStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing listUsersStmt: %w", cerr) - } - } - if q.updateUserStmt != nil { - if cerr := q.updateUserStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing updateUserStmt: %w", cerr) - } - } - return err -} - -func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (sql.Result, error) { - switch { - case stmt != nil && q.tx != nil: - return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...) - case stmt != nil: - return stmt.ExecContext(ctx, args...) - default: - return q.db.ExecContext(ctx, query, args...) - } -} - -func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) { - switch { - case stmt != nil && q.tx != nil: - return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...) - case stmt != nil: - return stmt.QueryContext(ctx, args...) - default: - return q.db.QueryContext(ctx, query, args...) - } -} - -func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) *sql.Row { - switch { - case stmt != nil && q.tx != nil: - return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...) - case stmt != nil: - return stmt.QueryRowContext(ctx, args...) - default: - return q.db.QueryRowContext(ctx, query, args...) - } -} - -type Queries struct { - db DBTX - tx *sql.Tx - createUserStmt *sql.Stmt - deleteUserStmt *sql.Stmt - getUserByEmailStmt *sql.Stmt - getUserByIDStmt *sql.Stmt - listUsersStmt *sql.Stmt - updateUserStmt *sql.Stmt -} - -func (q *Queries) WithTx(tx *sql.Tx) *Queries { - return &Queries{ - db: tx, - tx: tx, - createUserStmt: q.createUserStmt, - deleteUserStmt: q.deleteUserStmt, - getUserByEmailStmt: q.getUserByEmailStmt, - getUserByIDStmt: q.getUserByIDStmt, - listUsersStmt: q.listUsersStmt, - updateUserStmt: q.updateUserStmt, - } -} diff --git a/api/internal/db/user/models.go b/api/internal/db/user/models.go deleted file mode 100644 index bf03bb60..00000000 --- a/api/internal/db/user/models.go +++ /dev/null @@ -1,20 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.14.0 - -package user - -import ( - "database/sql" - "time" -) - -type User struct { - ID int64 `db:"id" json:"id"` - Username string `db:"username" json:"username"` - Email string `db:"email" json:"email"` - Password string `db:"password" json:"password"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` - DeletedAt sql.NullTime `db:"deleted_at" json:"deleted_at"` -} diff --git a/api/internal/db/user/query.sql b/api/internal/db/user/query.sql deleted file mode 100644 index a6219cca..00000000 --- a/api/internal/db/user/query.sql +++ /dev/null @@ -1,54 +0,0 @@ --- name: GetUserByID :one -SELECT - * -FROM - users -WHERE - id = ? -LIMIT - 1; - --- name: GetUserByEmail :one -SELECT - * -FROM - users -WHERE - email = ? -LIMIT - 1; - --- name: ListUsers :many -SELECT - * -FROM - users -ORDER BY - id; - --- name: CreateUser :execresult -INSERT INTO - users ( - username, - email, - password, - created_at, - updated_at, - deleted_at - ) -VALUES - (?, ?, ?, ?, ?, ?); - --- name: UpdateUser :execresult -UPDATE - users -SET - username = ? -WHERE - id = ?; - --- name: DeleteUser :exec -DELETE FROM - users -WHERE - id = ?; diff --git a/api/internal/db/user/query.sql.go b/api/internal/db/user/query.sql.go deleted file mode 100644 index f2ee335f..00000000 --- a/api/internal/db/user/query.sql.go +++ /dev/null @@ -1,168 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.14.0 -// source: query.sql - -package user - -import ( - "context" - "database/sql" - "time" -) - -const createUser = `-- name: CreateUser :execresult -INSERT INTO - users ( - username, - email, - password, - created_at, - updated_at, - deleted_at - ) -VALUES - (?, ?, ?, ?, ?, ?) -` - -type CreateUserParams struct { - Username string `db:"username" json:"username"` - Email string `db:"email" json:"email"` - Password string `db:"password" json:"password"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` - DeletedAt sql.NullTime `db:"deleted_at" json:"deleted_at"` -} - -func (q *Queries) CreateUser(ctx context.Context, arg *CreateUserParams) (sql.Result, error) { - return q.exec(ctx, q.createUserStmt, createUser, - arg.Username, - arg.Email, - arg.Password, - arg.CreatedAt, - arg.UpdatedAt, - arg.DeletedAt, - ) -} - -const deleteUser = `-- name: DeleteUser :exec -DELETE FROM - users -WHERE - id = ? -` - -func (q *Queries) DeleteUser(ctx context.Context, id int64) error { - _, err := q.exec(ctx, q.deleteUserStmt, deleteUser, id) - return err -} - -const getUserByEmail = `-- name: GetUserByEmail :one -SELECT - id, username, email, password, created_at, updated_at, deleted_at -FROM - users -WHERE - email = ? -LIMIT - 1 -` - -func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { - row := q.queryRow(ctx, q.getUserByEmailStmt, getUserByEmail, email) - var i User - err := row.Scan( - &i.ID, - &i.Username, - &i.Email, - &i.Password, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - ) - return i, err -} - -const getUserByID = `-- name: GetUserByID :one -SELECT - id, username, email, password, created_at, updated_at, deleted_at -FROM - users -WHERE - id = ? -LIMIT - 1 -` - -func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) { - row := q.queryRow(ctx, q.getUserByIDStmt, getUserByID, id) - var i User - err := row.Scan( - &i.ID, - &i.Username, - &i.Email, - &i.Password, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - ) - return i, err -} - -const listUsers = `-- name: ListUsers :many -SELECT - id, username, email, password, created_at, updated_at, deleted_at -FROM - users -ORDER BY - id -` - -func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { - rows, err := q.query(ctx, q.listUsersStmt, listUsers) - if err != nil { - return nil, err - } - defer rows.Close() - items := []User{} - for rows.Next() { - var i User - if err := rows.Scan( - &i.ID, - &i.Username, - &i.Email, - &i.Password, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateUser = `-- name: UpdateUser :execresult -UPDATE - users -SET - username = ? -WHERE - id = ? -` - -type UpdateUserParams struct { - Username string `db:"username" json:"username"` - ID int64 `db:"id" json:"id"` -} - -func (q *Queries) UpdateUser(ctx context.Context, arg *UpdateUserParams) (sql.Result, error) { - return q.exec(ctx, q.updateUserStmt, updateUser, arg.Username, arg.ID) -} diff --git a/api/internal/db/user/schema.sql b/api/internal/db/user/schema.sql deleted file mode 100644 index 1a447471..00000000 --- a/api/internal/db/user/schema.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE - users ( - id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, - username text NOT NULL, - email text NOT NULL, - password text NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - deleted_at TIMESTAMP NULL DEFAULT NULL, - UNIQUE (email) - ); diff --git a/api/jam.go b/api/jam.go index 498524eb..f0e80d72 100644 --- a/api/jam.go +++ b/api/jam.go @@ -1,8 +1,6 @@ package api import ( - "context" - "database/sql" "encoding/json" "io" "log" @@ -12,16 +10,18 @@ import ( "github.com/gobwas/ws" "github.com/gobwas/ws/wsutil" "github.com/google/uuid" - "github.com/rog-golang-buddies/rapidmidiex/api/internal/db/user" ) +// struct type to store info related to a websocket connection type jamConn struct { mu sync.Mutex conn io.ReadWriter - user.User + username string } +// struct type to store info related to a Jam session +// also contains a map of current connections to the session type jamSession struct { mu sync.RWMutex conns map[string]*jamConn @@ -30,26 +30,33 @@ type jamSession struct { id string name string tempo uint + owner string } +// struct type for the Jam service +// also contains current available sessions created by users type JamService struct { - DBCon *sql.DB mu sync.RWMutex sessions map[string]*jamSession } +// request types type ( newSessionReq struct { - Name string `json:"name"` - Tempo uint `json:"tempo"` + Username string `json:"username"` + SessionName string `json:"session_name"` + Tempo uint `json:"tempo"` } joinSessionReq struct { + Username string `json:"username"` SessionID string `json:"session_id"` } ) +// new session handler func (s *JamService) NewSession(w http.ResponseWriter, r *http.Request) { + // get values from the request si := newSessionReq{} if err := parse(r, &si); err != nil { log.Println(err) @@ -57,26 +64,48 @@ func (s *JamService) NewSession(w http.ResponseWriter, r *http.Request) { return } + // create a new session and set the provided username as the session owner session := jamSession{ conns: make(map[string]*jamConn), out: make(chan []interface{}), id: uuid.NewString(), - name: si.Name, + name: si.SessionName, tempo: si.Tempo, + owner: si.Username, } + // add session to sessions map s.addSession(&session) - w.WriteHeader(http.StatusOK) -} + // upgrade http connection to websocket + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } -func (s *JamService) JoinSession(w http.ResponseWriter, r *http.Request) { - email, ok := r.Context().Value(emailCtxKey).(string) - if !ok { - w.WriteHeader(http.StatusBadRequest) // TODO: shouldn't respond with bad request. + // create a new connection between the owner and the server + // then add it to the session connections + c := jamConn{} + c.conn = conn + c.username = si.Username + + // check for errors + // err isn't nil if the username is already used + err = session.addConn(&c) + if err != nil { + handlerError(w, err) return } + session.broadcast("Welcome to Rapidmidiex!") + w.WriteHeader(http.StatusOK) +} + +// join session handler +func (s *JamService) JoinSession(w http.ResponseWriter, r *http.Request) { + // get values from the request ji := joinSessionReq{} if err := parse(r, &ji); err != nil { log.Println(err) @@ -84,43 +113,56 @@ func (s *JamService) JoinSession(w http.ResponseWriter, r *http.Request) { return } - session := s.sessions[ji.SessionID] - conn, _, _, err := ws.UpgradeHTTP(r, w) + // check if session exists + session, err := s.getSession(ji.SessionID) if err != nil { - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) + handlerError(w, err) return } - c := jamConn{} - q := user.New(s.DBCon) - userInfo, err := q.GetUserByEmail(context.Background(), email) + + // upgrade http connection to websocket + conn, _, _, err := ws.UpgradeHTTP(r, w) if err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } - c.User = userInfo + + // create a new connection + c := jamConn{} c.conn = conn + c.username = ji.Username session.addConn(&c) - session.broadcast("Welcome " + userInfo.Username + "!") + session.broadcast("Welcome " + ji.Username + "!") w.WriteHeader(http.StatusOK) } func (s *JamService) addSession(js *jamSession) { s.mu.Lock() - { - s.sessions[js.id] = js - } + s.sessions[js.id] = js s.mu.Unlock() } -func (s *jamSession) addConn(jc *jamConn) { - s.mu.Lock() - { - s.conns[jc.Email] = jc +func (s *JamService) getSession(sID string) (*jamSession, error) { + session, ok := s.sessions[sID] + if ok { + return &jamSession{}, &errSessionNotFound + } + + return session, nil +} + +func (s *jamSession) addConn(jc *jamConn) error { + if _, ok := s.conns[jc.username]; ok { + return &errUsernameAlreadyUsed } + + s.mu.Lock() + s.conns[jc.username] = jc s.mu.Unlock() + + return nil } func (c *jamConn) write(i interface{}) error { @@ -158,13 +200,15 @@ func (c *jamConn) writeRaw(b []byte) error { // } // } +// iterates through session connections +// and send provided message to each of them func (s *jamSession) broadcast(i interface{}) { for _, c := range s.conns { select { case <-s.out: c.write(i) default: - delete(s.conns, c.Email) + delete(s.conns, c.username) // c.close() } } diff --git a/api/middlewares.go b/api/middlewares.go deleted file mode 100644 index 0ccdce61..00000000 --- a/api/middlewares.go +++ /dev/null @@ -1,37 +0,0 @@ -package api - -import ( - "context" - "net/http" - "strings" -) - -type ctxKey string - -var emailCtxKey = ctxKey("email") - -func (s *AuthService) CheckAuth(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - at := r.Header.Get(authorizationHeader) - bearer := strings.Split(at, " ") - if !(len(bearer) > 1) { - w.WriteHeader(http.StatusUnauthorized) - return - } - userInfo, err := parseAccessTokenWithValidate(bearer[1]) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - privateClaims := userInfo.PrivateClaims() - email, ok := privateClaims["email"].(string) - if !ok { - w.WriteHeader(http.StatusUnauthorized) - return - } - - ctx := context.WithValue(r.Context(), emailCtxKey, email) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} From a5bc5faf05b9a804562ae1051510259f9c1262bb Mon Sep 17 00:00:00 2001 From: Parham Moieni Date: Thu, 8 Sep 2022 22:13:56 +0430 Subject: [PATCH 003/246] fixed some bugs --- api/errors.go | 5 ++--- api/jam.go | 56 +++++++++++++++++++++++++-------------------------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/api/errors.go b/api/errors.go index 2eceee50..0c36d8bb 100644 --- a/api/errors.go +++ b/api/errors.go @@ -6,9 +6,8 @@ import ( ) var ( - errNoCookie = errorResponse{status: http.StatusUnauthorized, message: "Cookie not found."} - errUsernameAlreadyUsed = errorResponse{status: http.StatusForbidden, message: "This username is already used."} - errSessionNotFound = errorResponse{status: http.StatusNotFound, message: "Session not found."} + errNoCookie = errorResponse{status: http.StatusUnauthorized, message: "Cookie not found."} + errSessionNotFound = errorResponse{status: http.StatusNotFound, message: "Session not found."} ) func handlerError(w http.ResponseWriter, err error) { diff --git a/api/jam.go b/api/jam.go index f0e80d72..8df5acaf 100644 --- a/api/jam.go +++ b/api/jam.go @@ -17,6 +17,7 @@ type jamConn struct { mu sync.Mutex conn io.ReadWriter + id string username string } @@ -64,19 +65,6 @@ func (s *JamService) NewSession(w http.ResponseWriter, r *http.Request) { return } - // create a new session and set the provided username as the session owner - session := jamSession{ - conns: make(map[string]*jamConn), - out: make(chan []interface{}), - - id: uuid.NewString(), - name: si.SessionName, - tempo: si.Tempo, - owner: si.Username, - } - // add session to sessions map - s.addSession(&session) - // upgrade http connection to websocket conn, _, _, err := ws.UpgradeHTTP(r, w) if err != nil { @@ -89,15 +77,28 @@ func (s *JamService) NewSession(w http.ResponseWriter, r *http.Request) { // then add it to the session connections c := jamConn{} c.conn = conn - c.username = si.Username + c.id = uuid.NewString() + if isEmptyString(si.Username) { + c.username = c.id + } else { + c.username = si.Username + } + + // create a new session and set the session owner + session := jamSession{ + conns: make(map[string]*jamConn), + out: make(chan []interface{}), + id: uuid.NewString(), + name: si.SessionName, + tempo: si.Tempo, + owner: c.id, + } + // add session to sessions map + s.addSession(&session) // check for errors // err isn't nil if the username is already used - err = session.addConn(&c) - if err != nil { - handlerError(w, err) - return - } + session.addConn(&c) session.broadcast("Welcome to Rapidmidiex!") w.WriteHeader(http.StatusOK) @@ -131,7 +132,12 @@ func (s *JamService) JoinSession(w http.ResponseWriter, r *http.Request) { // create a new connection c := jamConn{} c.conn = conn - c.username = ji.Username + c.id = uuid.NewString() + if isEmptyString(ji.Username) { + c.username = c.id + } else { + c.username = ji.Username + } session.addConn(&c) session.broadcast("Welcome " + ji.Username + "!") @@ -153,16 +159,10 @@ func (s *JamService) getSession(sID string) (*jamSession, error) { return session, nil } -func (s *jamSession) addConn(jc *jamConn) error { - if _, ok := s.conns[jc.username]; ok { - return &errUsernameAlreadyUsed - } - +func (s *jamSession) addConn(jc *jamConn) { s.mu.Lock() - s.conns[jc.username] = jc + s.conns[jc.id] = jc s.mu.Unlock() - - return nil } func (c *jamConn) write(i interface{}) error { From 71abe97a93a291fd114a2c33e68bf4298b4cedfb Mon Sep 17 00:00:00 2001 From: Parham Moieni Date: Thu, 8 Sep 2022 22:16:26 +0430 Subject: [PATCH 004/246] fixed some bugs --- cmd/server/main.go | 68 +--------------------------------------------- 1 file changed, 1 insertion(+), 67 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 7eea3675..3cce0ca2 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,19 +1,12 @@ package main import ( - "database/sql" - "fmt" "log" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" - "github.com/go-redis/redis/v9" "github.com/rog-golang-buddies/rapidmidiex" "github.com/rog-golang-buddies/rapidmidiex/api" - migrate "github.com/rubenv/sql-migrate" - "github.com/spf13/viper" - - _ "github.com/go-sql-driver/mysql" ) func main() { @@ -22,54 +15,7 @@ func main() { log.Fatalf("failed to read config: %v", err.Error()) } - dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true", - viper.GetString("DB_USER"), - viper.GetString("DB_PASSWORD"), - viper.GetString("DB_HOST"), - viper.GetString("DB_PORT"), - viper.GetString("DB_NAME"), - ) - - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatalln(err) - } - - // run db migrations - migrations := &migrate.FileMigrationSource{ - Dir: "api/internal/db/migration", - } - - _, err = migrate.Exec(db, "mysql", migrations, migrate.Up) - if err != nil { - log.Fatalln(err) - } - - rtdb := redis.NewClient(&redis.Options{ - Addr: viper.GetString("REDIS_ADDR"), - Password: viper.GetString("REDIS_PASSWORD"), - DB: 0, - }) - cidb := redis.NewClient(&redis.Options{ - Addr: viper.GetString("REDIS_ADDR"), - Password: viper.GetString("REDIS_PASSWORD"), - DB: 1, - }) - ptdb := redis.NewClient(&redis.Options{ - Addr: viper.GetString("REDIS_ADDR"), - Password: viper.GetString("REDIS_PASSWORD"), - DB: 2, - }) - - authService := api.AuthService{ - DBCon: db, - RedisRefreshTokenDB: rtdb, - RedisClientIDDB: cidb, - RedisPasswordTokenDB: ptdb, - } - jamService := api.JamService{ - DBCon: db, - } + jamService := api.JamService{} server := api.Server{ Port: ":8080", @@ -78,22 +24,10 @@ func main() { server.Router.Route("/api/v1", func(r chi.Router) { r.Use(middleware.Logger) - r.Route("/auth", func(r chi.Router) { - r.Post("/register", authService.Register) - r.Post("/login", authService.Login) - r.Get("/refresh_token", authService.RefreshToken) - r.Get("/logout", authService.Logout) - }) r.Route("/jam", func(r chi.Router) { - r.Use(authService.CheckAuth) r.Post("/new", jamService.NewSession) r.Get("/{session_id}/join", jamService.JoinSession) }) - r.Route("/users", func(r chi.Router) { - r.Use(authService.CheckAuth) - r.Get("/me", authService.GetUserInfo) - r.Patch("/me", authService.UpdateUserInfo) - }) }) log.Println("starting the server") From 1119927b79227f001efb0450a09f5856f8df5246 Mon Sep 17 00:00:00 2001 From: Parham Moieni Date: Fri, 9 Sep 2022 12:07:06 +0430 Subject: [PATCH 005/246] removed client --- client/.gitignore | 24 -- client/README.md | 48 --- client/index.html | 13 - client/package.json | 23 -- client/public/vite.svg | 1 - client/src/App.svelte | 9 - client/src/assets/svelte.svg | 1 - client/src/main.ts | 8 - client/src/vite-env.d.ts | 2 - client/svelte.config.js | 7 - client/tsconfig.json | 21 - client/tsconfig.node.json | 8 - client/vite.config.ts | 7 - client/yarn.lock | 733 ----------------------------------- 14 files changed, 905 deletions(-) delete mode 100644 client/.gitignore delete mode 100644 client/README.md delete mode 100644 client/index.html delete mode 100644 client/package.json delete mode 100644 client/public/vite.svg delete mode 100644 client/src/App.svelte delete mode 100644 client/src/assets/svelte.svg delete mode 100644 client/src/main.ts delete mode 100644 client/src/vite-env.d.ts delete mode 100644 client/svelte.config.js delete mode 100644 client/tsconfig.json delete mode 100644 client/tsconfig.node.json delete mode 100644 client/vite.config.ts delete mode 100644 client/yarn.lock diff --git a/client/.gitignore b/client/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/client/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/client/README.md b/client/README.md deleted file mode 100644 index 4ef762ff..00000000 --- a/client/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Svelte + TS + Vite - -This template should help get you started developing with Svelte and TypeScript in Vite. - -## Recommended IDE Setup - -[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). - -## Need an official Svelte framework? - -Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. - -## Technical considerations - -**Why use this over SvelteKit?** - -- It brings its own routing solution which might not be preferable for some users. -- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. - `vite dev` and `vite build` wouldn't work in a SvelteKit environment, for example. - -This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. - -Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. - -**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** - -Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. - -**Why include `.vscode/extensions.json`?** - -Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. - -**Why enable `allowJs` in the TS template?** - -While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. - -**Why is HMR not preserving my local component state?** - -HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). - -If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. - -```ts -// store.ts -// An extremely simple external store -import { writable } from 'svelte/store' -export default writable(0) -``` diff --git a/client/index.html b/client/index.html deleted file mode 100644 index b5b12526..00000000 --- a/client/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + Svelte + TS - - -
- - - diff --git a/client/package.json b/client/package.json deleted file mode 100644 index e9ba764d..00000000 --- a/client/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "client", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "check": "svelte-check --tsconfig ./tsconfig.json" - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^1.0.1", - "@tsconfig/svelte": "^3.0.0", - "sass": "^1.54.8", - "svelte": "^3.49.0", - "svelte-check": "^2.8.0", - "svelte-preprocess": "^4.10.7", - "tslib": "^2.4.0", - "typescript": "^4.6.4", - "vite": "^3.0.7" - } -} diff --git a/client/public/vite.svg b/client/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/client/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/App.svelte b/client/src/App.svelte deleted file mode 100644 index 770a5afc..00000000 --- a/client/src/App.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - -
- - diff --git a/client/src/assets/svelte.svg b/client/src/assets/svelte.svg deleted file mode 100644 index c5e08481..00000000 --- a/client/src/assets/svelte.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/main.ts b/client/src/main.ts deleted file mode 100644 index 5c1f795f..00000000 --- a/client/src/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import './app.css' -import App from './App.svelte' - -const app = new App({ - target: document.getElementById('app') -}) - -export default app diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts deleted file mode 100644 index 4078e747..00000000 --- a/client/src/vite-env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/client/svelte.config.js b/client/svelte.config.js deleted file mode 100644 index 3630bb39..00000000 --- a/client/svelte.config.js +++ /dev/null @@ -1,7 +0,0 @@ -import sveltePreprocess from 'svelte-preprocess' - -export default { - // Consult https://github.com/sveltejs/svelte-preprocess - // for more information about preprocessors - preprocess: sveltePreprocess() -} diff --git a/client/tsconfig.json b/client/tsconfig.json deleted file mode 100644 index d3830319..00000000 --- a/client/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "@tsconfig/svelte/tsconfig.json", - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "module": "ESNext", - "resolveJsonModule": true, - "baseUrl": ".", - /** - * Typecheck JS in `.svelte` and `.js` files by default. - * Disable checkJs if you'd like to use dynamic types in JS. - * Note that setting allowJs false does not prevent the use - * of JS in `.svelte` files. - */ - "allowJs": true, - "checkJs": true, - "isolatedModules": true - }, - "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json deleted file mode 100644 index 65dbdb96..00000000 --- a/client/tsconfig.node.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "module": "ESNext", - "moduleResolution": "Node" - }, - "include": ["vite.config.ts"] -} diff --git a/client/vite.config.ts b/client/vite.config.ts deleted file mode 100644 index 401b4d4b..00000000 --- a/client/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite' -import { svelte } from '@sveltejs/vite-plugin-svelte' - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [svelte()] -}) diff --git a/client/yarn.lock b/client/yarn.lock deleted file mode 100644 index 0a2e328b..00000000 --- a/client/yarn.lock +++ /dev/null @@ -1,733 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@esbuild/linux-loong64@0.14.54": - version "0.14.54" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028" - integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw== - -"@jridgewell/resolve-uri@^3.0.3": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== - -"@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/trace-mapping@^0.3.9": - version "0.3.15" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" - integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@rollup/pluginutils@^4.2.1": - version "4.2.1" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" - integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== - dependencies: - estree-walker "^2.0.1" - picomatch "^2.2.2" - -"@sveltejs/vite-plugin-svelte@^1.0.1": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.4.tgz#363a0adeb9221c35abb65197c6db0754b9994a08" - integrity sha512-UZco2fdj0OVuRWC0SUJjEOftITc2IeHLFJNp00ym9MuQ9dShnlO4P29G8KUxRlcS7kSpzHuko6eCR9MOALj7lQ== - dependencies: - "@rollup/pluginutils" "^4.2.1" - debug "^4.3.4" - deepmerge "^4.2.2" - kleur "^4.1.5" - magic-string "^0.26.2" - svelte-hmr "^0.14.12" - -"@tsconfig/svelte@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@tsconfig/svelte/-/svelte-3.0.0.tgz#b06e059209f04c414de0069f2f0e2796d979fc6f" - integrity sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg== - -"@types/node@*": - version "18.7.14" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.14.tgz#0fe081752a3333392d00586d815485a17c2cf3c9" - integrity sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA== - -"@types/pug@^2.0.4": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6" - integrity sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg== - -"@types/sass@^1.16.0": - version "1.43.1" - resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.43.1.tgz#86bb0168e9e881d7dade6eba16c9ed6d25dc2f68" - integrity sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g== - dependencies: - "@types/node" "*" - -anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -buffer-crc32@^0.2.5: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.1: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -deepmerge@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== - -detect-indent@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" - integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== - -es6-promise@^3.1.2: - version "3.3.1" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" - integrity sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg== - -esbuild-android-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be" - integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ== - -esbuild-android-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771" - integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg== - -esbuild-darwin-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25" - integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug== - -esbuild-darwin-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73" - integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw== - -esbuild-freebsd-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d" - integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg== - -esbuild-freebsd-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48" - integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q== - -esbuild-linux-32@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5" - integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw== - -esbuild-linux-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652" - integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg== - -esbuild-linux-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b" - integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig== - -esbuild-linux-arm@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59" - integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw== - -esbuild-linux-mips64le@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34" - integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw== - -esbuild-linux-ppc64le@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e" - integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ== - -esbuild-linux-riscv64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8" - integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg== - -esbuild-linux-s390x@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6" - integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA== - -esbuild-netbsd-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81" - integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w== - -esbuild-openbsd-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b" - integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw== - -esbuild-sunos-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da" - integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw== - -esbuild-windows-32@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31" - integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w== - -esbuild-windows-64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4" - integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ== - -esbuild-windows-arm64@0.14.54: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982" - integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg== - -esbuild@^0.14.47: - version "0.14.54" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2" - integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA== - optionalDependencies: - "@esbuild/linux-loong64" "0.14.54" - esbuild-android-64 "0.14.54" - esbuild-android-arm64 "0.14.54" - esbuild-darwin-64 "0.14.54" - esbuild-darwin-arm64 "0.14.54" - esbuild-freebsd-64 "0.14.54" - esbuild-freebsd-arm64 "0.14.54" - esbuild-linux-32 "0.14.54" - esbuild-linux-64 "0.14.54" - esbuild-linux-arm "0.14.54" - esbuild-linux-arm64 "0.14.54" - esbuild-linux-mips64le "0.14.54" - esbuild-linux-ppc64le "0.14.54" - esbuild-linux-riscv64 "0.14.54" - esbuild-linux-s390x "0.14.54" - esbuild-netbsd-64 "0.14.54" - esbuild-openbsd-64 "0.14.54" - esbuild-sunos-64 "0.14.54" - esbuild-windows-32 "0.14.54" - esbuild-windows-64 "0.14.54" - esbuild-windows-arm64 "0.14.54" - -estree-walker@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" - integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== - -fast-glob@^3.2.7: - version "3.2.11" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" - integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fastq@^1.6.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" - integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== - dependencies: - reusify "^1.0.4" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -glob-parent@^5.1.2, glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob@^7.1.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -graceful-fs@^4.1.3: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -immutable@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef" - integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ== - -import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-core-module@^2.9.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" - integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== - dependencies: - has "^1.0.3" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -kleur@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" - integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== - -magic-string@^0.25.7: - version "0.25.9" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" - integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== - dependencies: - sourcemap-codec "^1.4.8" - -magic-string@^0.26.2: - version "0.26.3" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.3.tgz#25840b875140f7b4785ab06bddc384270b7dd452" - integrity sha512-u1Po0NDyFcwdg2nzHT88wSK0+Rih0N1M+Ph1Sp08k8yvFFU3KR72wryS7e1qMPJypt99WB7fIFVCA92mQrMjrg== - dependencies: - sourcemap-codec "^1.4.8" - -merge2@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^4.0.4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== - dependencies: - braces "^3.0.2" - picomatch "^2.3.1" - -min-indent@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" - integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== - -minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== - -mkdirp@^0.5.1: - version "0.5.6" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" - integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== - dependencies: - minimist "^1.2.6" - -mri@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" - integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -nanoid@^3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" - integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -postcss@^8.4.16: - version "8.4.16" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c" - integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ== - dependencies: - nanoid "^3.3.4" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve@^1.22.1: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== - dependencies: - is-core-module "^2.9.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rimraf@^2.5.2: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -"rollup@>=2.75.6 <2.77.0 || ~2.77.0": - version "2.77.3" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.3.tgz#8f00418d3a2740036e15deb653bed1a90ee0cc12" - integrity sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g== - optionalDependencies: - fsevents "~2.3.2" - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -sade@^1.7.4: - version "1.8.1" - resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" - integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== - dependencies: - mri "^1.1.0" - -sander@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/sander/-/sander-0.5.1.tgz#741e245e231f07cafb6fdf0f133adfa216a502ad" - integrity sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA== - dependencies: - es6-promise "^3.1.2" - graceful-fs "^4.1.3" - mkdirp "^0.5.1" - rimraf "^2.5.2" - -sass@^1.54.8: - version "1.54.8" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.54.8.tgz#4adef0dd86ea2b1e4074f551eeda4fc5f812a996" - integrity sha512-ib4JhLRRgbg6QVy6bsv5uJxnJMTS2soVcCp9Y88Extyy13A8vV0G1fAwujOzmNkFQbR3LvedudAMbtuNRPbQww== - dependencies: - chokidar ">=3.0.0 <4.0.0" - immutable "^4.0.0" - source-map-js ">=0.6.2 <2.0.0" - -sorcery@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.10.0.tgz#8ae90ad7d7cb05fc59f1ab0c637845d5c15a52b7" - integrity sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g== - dependencies: - buffer-crc32 "^0.2.5" - minimist "^1.2.0" - sander "^0.5.0" - sourcemap-codec "^1.3.0" - -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== - -sourcemap-codec@^1.3.0, sourcemap-codec@^1.4.8: - version "1.4.8" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" - integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== - -strip-indent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" - integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== - dependencies: - min-indent "^1.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -svelte-check@^2.8.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-2.9.0.tgz#94a4bbe5bccade5a9edae987f417040ab7af4a2a" - integrity sha512-9AVrtP7WbfDgCdqTZNPdj5CCCy1OrYMxFVWAWzNw7fl93c9klFJFtqzVXa6fovfQ050CcpUyJE2dPFL9TFAREw== - dependencies: - "@jridgewell/trace-mapping" "^0.3.9" - chokidar "^3.4.1" - fast-glob "^3.2.7" - import-fresh "^3.2.1" - picocolors "^1.0.0" - sade "^1.7.4" - svelte-preprocess "^4.0.0" - typescript "*" - -svelte-hmr@^0.14.12: - version "0.14.12" - resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.14.12.tgz#a127aec02f1896500b10148b2d4d21ddde39973f" - integrity sha512-4QSW/VvXuqVcFZ+RhxiR8/newmwOCTlbYIezvkeN6302YFRE8cXy0naamHcjz8Y9Ce3ITTZtrHrIL0AGfyo61w== - -svelte-preprocess@^4.0.0, svelte-preprocess@^4.10.7: - version "4.10.7" - resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-4.10.7.tgz#3626de472f51ffe20c9bc71eff5a3da66797c362" - integrity sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw== - dependencies: - "@types/pug" "^2.0.4" - "@types/sass" "^1.16.0" - detect-indent "^6.0.0" - magic-string "^0.25.7" - sorcery "^0.10.0" - strip-indent "^3.0.0" - -svelte@^3.49.0: - version "3.50.0" - resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.50.0.tgz#d11a7a6bd1e084ec051d55104a9af8bccf54461f" - integrity sha512-zXeOUDS7+85i+RxLN+0iB6PMbGH7OhEgjETcD1fD8ZrhuhNFxYxYEHU41xuhkHIulJavcu3PKbPyuCrBxdxskQ== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -tslib@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" - integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== - -typescript@*, typescript@^4.6.4: - version "4.8.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790" - integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw== - -vite@^3.0.7: - version "3.0.9" - resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.9.tgz#45fac22c2a5290a970f23d66c1aef56a04be8a30" - integrity sha512-waYABTM+G6DBTCpYAxvevpG50UOlZuynR0ckTK5PawNVt7ebX6X7wNXHaGIO6wYYFXSM7/WcuFuO2QzhBB6aMw== - dependencies: - esbuild "^0.14.47" - postcss "^8.4.16" - resolve "^1.22.1" - rollup ">=2.75.6 <2.77.0 || ~2.77.0" - optionalDependencies: - fsevents "~2.3.2" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== From c6d2a553c9801b91662ea021666c40206c5ce311 Mon Sep 17 00:00:00 2001 From: Parham Moieni Date: Fri, 9 Sep 2022 14:45:43 +0430 Subject: [PATCH 006/246] use session id from url param --- api/jam.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api/jam.go b/api/jam.go index 8df5acaf..fb54afc3 100644 --- a/api/jam.go +++ b/api/jam.go @@ -7,6 +7,7 @@ import ( "net/http" "sync" + "github.com/go-chi/chi/v5" "github.com/gobwas/ws" "github.com/gobwas/ws/wsutil" "github.com/google/uuid" @@ -114,8 +115,10 @@ func (s *JamService) JoinSession(w http.ResponseWriter, r *http.Request) { return } - // check if session exists - session, err := s.getSession(ji.SessionID) + sID := chi.URLParam(r, "session_id") + + // err isn't nil if session doesn't exist + session, err := s.getSession(sID) if err != nil { handlerError(w, err) return From 77f7ed7ef9fcd57f3cf06a3eb007166ff0b61deb Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Tue, 13 Sep 2022 10:38:09 -0400 Subject: [PATCH 007/246] Ignore all config files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index dd593eaf..91faf9d5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ bin/ # Other .DS_Store *.env +cmd/**/config From c46c1ca21aa68b8cfc088865e28896bf600312e2 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Tue, 13 Sep 2022 10:38:46 -0400 Subject: [PATCH 008/246] Use PORT env var --- cmd/server/main.go | 5 +++-- config.go | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 3cce0ca2..b9dd94db 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -5,8 +5,9 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" - "github.com/rog-golang-buddies/rapidmidiex" + rmx "github.com/rog-golang-buddies/rapidmidiex" "github.com/rog-golang-buddies/rapidmidiex/api" + "github.com/spf13/viper" ) func main() { @@ -18,7 +19,7 @@ func main() { jamService := api.JamService{} server := api.Server{ - Port: ":8080", + Port: ":" + viper.GetString("PORT"), Router: chi.NewMux(), } diff --git a/config.go b/config.go index 7bc1064f..09b703a9 100644 --- a/config.go +++ b/config.go @@ -7,6 +7,8 @@ func LoadConfig() error { viper.SetConfigType("env") // REQUIRED if the config file does not have the extension in the name viper.AddConfigPath(".") // optionally look for config in the working directory + viper.SetDefault("PORT", "8080") // Set Default variables + viper.AutomaticEnv() err := viper.ReadInConfig() // Find and read the config file From 127f3e2370b5d854cdb669b8c54eef06e66e3591 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Tue, 13 Sep 2022 14:50:04 -0400 Subject: [PATCH 009/246] Websocket Route setup --- api/jam.go | 71 +++++++++++-- api/jam_test.go | 62 ++++++++++++ api/server.go | 18 ++++ cmd/server/main.go | 5 +- go.mod | 20 +--- go.sum | 242 +-------------------------------------------- 6 files changed, 153 insertions(+), 265 deletions(-) create mode 100644 api/jam_test.go diff --git a/api/jam.go b/api/jam.go index fb54afc3..f4c8abac 100644 --- a/api/jam.go +++ b/api/jam.go @@ -44,25 +44,37 @@ type JamService struct { // request types type ( - newSessionReq struct { + newJamReq struct { Username string `json:"username"` SessionName string `json:"session_name"` Tempo uint `json:"tempo"` } - joinSessionReq struct { + joinJamReq struct { Username string `json:"username"` SessionID string `json:"session_id"` } + wsReq struct { + MessageType string `json:"messageType"` // Type of application message. Ex: JAM_SESSION_CONNECT, JAM_SESSION_CREATE + Payload struct{} `json:"payload"` // Payload of message, differs according to MessageType. Ex: JAM_SESSION_CREATE may contain Jam Session config (tempo, session name, etc). + } + + JamSlim struct { + Id string `json:"id"` + Name string `json:"name"` + } + listJamsResp struct { + Jams []JamSlim + } ) // new session handler func (s *JamService) NewSession(w http.ResponseWriter, r *http.Request) { // get values from the request - si := newSessionReq{} + si := newJamReq{} if err := parse(r, &si); err != nil { log.Println(err) - w.WriteHeader(http.StatusInternalServerError) + w.WriteHeader(http.StatusBadRequest) return } @@ -105,13 +117,60 @@ func (s *JamService) NewSession(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } +// Connect establishes a WebSocket connection with the application. From there we can communicate with the client about which session to join. +func (s *JamService) Connect(w http.ResponseWriter, r *http.Request) { + // upgrade http connection to websocket + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + log.Println("connect", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + go func() { + defer conn.Close() + var ( + fr = wsutil.NewReader(conn, ws.StateServerSide) + fw = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText) + decoder = json.NewDecoder(fr) + encoder = json.NewEncoder(fw) + ) + for { + hdr, err := fr.NextFrame() + if err != nil { + log.Println(err) + } + if hdr.OpCode == ws.OpClose { + log.Println(io.EOF) + // Break out of for and close the connection + return + } + + // TODO: Hand off messages to some app level message broker + // Ex: JAM_SESSION_CONNECT, JAM_SESSION_CREATE + var req wsReq + if err := decoder.Decode(&req); err != nil { + log.Println(err) + } + + resp := listJamsResp{Jams: make([]JamSlim, 0)} + if err := encoder.Encode(&resp); err != nil { + log.Println(err) + } + if err = fw.Flush(); err != nil { + log.Println(err) + } + } + }() +} + // join session handler func (s *JamService) JoinSession(w http.ResponseWriter, r *http.Request) { // get values from the request - ji := joinSessionReq{} + ji := joinJamReq{} if err := parse(r, &ji); err != nil { log.Println(err) - w.WriteHeader(http.StatusInternalServerError) + w.WriteHeader(http.StatusBadRequest) return } diff --git a/api/jam_test.go b/api/jam_test.go new file mode 100644 index 00000000..adefed72 --- /dev/null +++ b/api/jam_test.go @@ -0,0 +1,62 @@ +package api + +import ( + "context" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" +) + +func TestConnect(t *testing.T) { + t.Run("we get a list of available Jam Session when we connect over websocket", func(t *testing.T) { + + server := httptest.NewServer(NewServer(":9005").Router) + + wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws/v1/jam/new" + + conn, _, _, err := ws.DefaultDialer.Dial(context.Background(), wsURL) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + defer server.Close() + defer conn.Close() + + _, err = conn.Write([]byte("{}")) + if err != nil { + t.Fatal("could not write to WS connection") + } + + within(t, time.Millisecond*10, func() { + msg, err := wsutil.ReadServerText(conn) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + want := `{"Jams":[]}` + "\n" + if string(msg) != want { + t.Errorf(`got "%s", want "%s"`, string(msg), want) + } + }) + }) +} + +func within(t testing.TB, d time.Duration, assert func()) { + t.Helper() + + done := make(chan struct{}, 1) + + go func() { + assert() + done <- struct{}{} + }() + + select { + case <-time.After(d): + t.Error("timed out") + case <-done: + } +} diff --git a/api/server.go b/api/server.go index 24abbc38..33153fa9 100644 --- a/api/server.go +++ b/api/server.go @@ -9,6 +9,7 @@ import ( "syscall" "time" + "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" "github.com/rs/cors" "golang.org/x/sync/errgroup" @@ -64,3 +65,20 @@ func (s *Server) ServeHTTP() { log.Printf("exit reason: %s \n", err) } } + +func NewServer(port string) *Server { + jamService := JamService{} + s := new(Server) + + s.Port = port + s.Router = chi.NewMux() + + s.Router.Route("/ws/v1", func(r chi.Router) { + r.Use(middleware.Logger) + r.Route("/jam", func(r chi.Router) { + r.Get("/new", jamService.Connect) + }) + }) + + return s +} diff --git a/cmd/server/main.go b/cmd/server/main.go index b9dd94db..ce28aa3e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -23,11 +23,10 @@ func main() { Router: chi.NewMux(), } - server.Router.Route("/api/v1", func(r chi.Router) { + server.Router.Route("/ws/v1", func(r chi.Router) { r.Use(middleware.Logger) r.Route("/jam", func(r chi.Router) { - r.Post("/new", jamService.NewSession) - r.Get("/{session_id}/join", jamService.JoinSession) + r.Get("/new", jamService.Connect) }) }) diff --git a/go.mod b/go.mod index 0999dd46..f7162007 100644 --- a/go.mod +++ b/go.mod @@ -3,46 +3,34 @@ module github.com/rog-golang-buddies/rapidmidiex go 1.18 require ( + github.com/go-chi/chi v1.5.4 github.com/go-chi/chi/v5 v5.0.7 - github.com/go-redis/redis/v9 v9.0.0-beta.2 - github.com/go-sql-driver/mysql v1.6.0 github.com/gobwas/ws v1.1.0 github.com/google/uuid v1.3.0 - github.com/lestrrat-go/jwx/v2 v2.0.6 - github.com/pmoieni/nimbus-cloud v0.0.0-20220826211613-3fdd55558ccb github.com/rs/cors v1.8.2 - github.com/rubenv/sql-migrate v1.1.2 github.com/spf13/viper v1.12.0 - golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde ) require ( - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/go-gorp/gorp/v3 v3.0.2 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect - github.com/goccy/go-json v0.9.11 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/lestrrat-go/blackmagic v1.0.1 // indirect - github.com/lestrrat-go/httpcc v1.0.1 // indirect - github.com/lestrrat-go/httprc v1.0.4 // indirect - github.com/lestrrat-go/iter v1.0.2 // indirect - github.com/lestrrat-go/option v1.0.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect + github.com/rogpeppe/go-internal v1.8.0 // indirect github.com/spf13/afero v1.8.2 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.8.0 // indirect github.com/subosito/gotenv v1.3.0 // indirect golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/text v0.3.7 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 5af95138..5bf669b2 100644 --- a/go.sum +++ b/go.sum @@ -17,9 +17,6 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -28,7 +25,6 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -42,18 +38,7 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -61,63 +46,31 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= -github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gorp/gorp/v3 v3.0.2 h1:ULqJXIekoqMx29FI5ekXXFoH1dT2Vc8UhnRzBg+Emz4= -github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-redis/redis/v9 v9.0.0-beta.2 h1:ZSr84TsnQyKMAg8gnV+oawuQezeJR11/09THcWCQzr4= -github.com/go-redis/redis/v9 v9.0.0-beta.2/go.mod h1:Bldcd/M/bm9HbnNPi/LUtYBSD8ttcZYBMupwMXhdU0o= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= -github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= -github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= -github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= -github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= -github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= -github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= -github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -129,7 +82,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -144,9 +96,6 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -157,11 +106,8 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -175,8 +121,6 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= @@ -184,43 +128,15 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= -github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -228,156 +144,63 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= -github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= -github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= -github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= -github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= -github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= -github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= -github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.0.6 h1:RlyYNLV892Ed7+FTfj1ROoF6x7WxL965PGTHso/60G0= -github.com/lestrrat-go/jwx/v2 v2.0.6/go.mod h1:aVrGuwEr3cp2Prw6TtQvr8sQxe+84gruID5C9TxT64Q= -github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= -github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= -github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= -github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= -github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= -github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= -github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= -github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmoieni/nimbus-cloud v0.0.0-20220826211613-3fdd55558ccb h1:Kek4TR8il3xlCQfbY6yELvpmwhj6heS4Gn+GsSSqy2Y= -github.com/pmoieni/nimbus-cloud v0.0.0-20220826211613-3fdd55558ccb/go.mod h1:cp4EDSZDp5QDVIgH52Ibn/iZ+MGIddrpDqMzBXPSsy4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1 h1:oL4IBbcqwhhNWh31bjOX8C/OCy0zs9906d/VUru+bqg= -github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rubenv/sql-migrate v1.1.2 h1:9M6oj4e//owVVHYrFISmY9LBRw6gzkCNmD9MV36tZeQ= -github.com/rubenv/sql-migrate v1.1.2/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= -github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -401,7 +224,6 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -412,11 +234,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -443,16 +262,9 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -462,9 +274,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -475,12 +284,9 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -490,8 +296,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -514,32 +318,19 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -551,7 +342,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -561,7 +351,6 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -584,7 +373,6 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -593,11 +381,8 @@ golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -621,9 +406,6 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -654,7 +436,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -667,13 +448,7 @@ google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -687,13 +462,9 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -704,26 +475,17 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 115cc5e60c6586f25460160426e2fad795cf3aa8 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Tue, 13 Sep 2022 15:32:07 -0400 Subject: [PATCH 010/246] Remove /new from endpoint --- api/jam_test.go | 4 ++-- api/server.go | 4 +--- cmd/server/main.go | 16 +--------------- 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/api/jam_test.go b/api/jam_test.go index adefed72..438f1d6e 100644 --- a/api/jam_test.go +++ b/api/jam_test.go @@ -16,7 +16,7 @@ func TestConnect(t *testing.T) { server := httptest.NewServer(NewServer(":9005").Router) - wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws/v1/jam/new" + wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws/v1/jam" conn, _, _, err := ws.DefaultDialer.Dial(context.Background(), wsURL) if err != nil { @@ -25,7 +25,7 @@ func TestConnect(t *testing.T) { defer server.Close() defer conn.Close() - _, err = conn.Write([]byte("{}")) + _, err = conn.Write([]byte(`{"type": "LIST_JAMS"}`)) if err != nil { t.Fatal("could not write to WS connection") } diff --git a/api/server.go b/api/server.go index 33153fa9..e95a0747 100644 --- a/api/server.go +++ b/api/server.go @@ -75,9 +75,7 @@ func NewServer(port string) *Server { s.Router.Route("/ws/v1", func(r chi.Router) { r.Use(middleware.Logger) - r.Route("/jam", func(r chi.Router) { - r.Get("/new", jamService.Connect) - }) + r.Get("/jam", jamService.Connect) }) return s diff --git a/cmd/server/main.go b/cmd/server/main.go index ce28aa3e..3156ea2b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -3,8 +3,6 @@ package main import ( "log" - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" rmx "github.com/rog-golang-buddies/rapidmidiex" "github.com/rog-golang-buddies/rapidmidiex/api" "github.com/spf13/viper" @@ -16,19 +14,7 @@ func main() { log.Fatalf("failed to read config: %v", err.Error()) } - jamService := api.JamService{} - - server := api.Server{ - Port: ":" + viper.GetString("PORT"), - Router: chi.NewMux(), - } - - server.Router.Route("/ws/v1", func(r chi.Router) { - r.Use(middleware.Logger) - r.Route("/jam", func(r chi.Router) { - r.Get("/new", jamService.Connect) - }) - }) + server := api.NewServer(":" + viper.GetString("PORT")) log.Println("starting the server") server.ServeHTTP() From 08941e12768017c2e97386de4d6d9b0f33aee2f3 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Tue, 13 Sep 2022 17:35:45 -0400 Subject: [PATCH 011/246] Remove port arg from NewServer() --- api/jam_test.go | 5 +++-- api/server.go | 3 +-- cmd/server/main.go | 9 ++++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/api/jam_test.go b/api/jam_test.go index 438f1d6e..fdb5d1aa 100644 --- a/api/jam_test.go +++ b/api/jam_test.go @@ -14,7 +14,7 @@ import ( func TestConnect(t *testing.T) { t.Run("we get a list of available Jam Session when we connect over websocket", func(t *testing.T) { - server := httptest.NewServer(NewServer(":9005").Router) + server := httptest.NewServer(NewServer().Router) wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws/v1/jam" @@ -25,7 +25,8 @@ func TestConnect(t *testing.T) { defer server.Close() defer conn.Close() - _, err = conn.Write([]byte(`{"type": "LIST_JAMS"}`)) + listJamsReq := []byte(`{"type": "LIST_JAMS"}`) + _, err = conn.Write(listJamsReq) if err != nil { t.Fatal("could not write to WS connection") } diff --git a/api/server.go b/api/server.go index e95a0747..96d46350 100644 --- a/api/server.go +++ b/api/server.go @@ -66,11 +66,10 @@ func (s *Server) ServeHTTP() { } } -func NewServer(port string) *Server { +func NewServer() *Server { jamService := JamService{} s := new(Server) - s.Port = port s.Router = chi.NewMux() s.Router.Route("/ws/v1", func(r chi.Router) { diff --git a/cmd/server/main.go b/cmd/server/main.go index 3156ea2b..dd575014 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,6 +2,7 @@ package main import ( "log" + "net/http" rmx "github.com/rog-golang-buddies/rapidmidiex" "github.com/rog-golang-buddies/rapidmidiex/api" @@ -14,8 +15,10 @@ func main() { log.Fatalf("failed to read config: %v", err.Error()) } - server := api.NewServer(":" + viper.GetString("PORT")) + server := api.NewServer() - log.Println("starting the server") - server.ServeHTTP() + port := ":" + viper.GetString("PORT") + log.Printf("starting the server on %s%s\n", server.Host, port) + + log.Fatal(http.ListenAndServe(port, server.Router)) } From 85ae6c91cbc30441a223a225a2e912747558ac80 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Wed, 14 Sep 2022 11:11:53 -0400 Subject: [PATCH 012/246] Correct message type; Add VS Code launch.json --- .vscode/launch.json | 15 +++++++++++++++ api/jam_test.go | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..73c99c11 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Run Server", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/server/main.go" + } + ] +} diff --git a/api/jam_test.go b/api/jam_test.go index fdb5d1aa..de71927c 100644 --- a/api/jam_test.go +++ b/api/jam_test.go @@ -25,7 +25,7 @@ func TestConnect(t *testing.T) { defer server.Close() defer conn.Close() - listJamsReq := []byte(`{"type": "LIST_JAMS"}`) + listJamsReq := []byte(`{"messageType": "JAM_LIST"}`) _, err = conn.Write(listJamsReq) if err != nil { t.Fatal("could not write to WS connection") From d8e05d8ad6f060cca3974e22d6eb304e995364ac Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Wed, 14 Sep 2022 13:34:46 -0400 Subject: [PATCH 013/246] Add Jam endpoint with hardcoded single jam --- api/errors.go | 1 + api/jam.go | 99 +++++++++++++++++++++++++++++++++++++++++++++- api/jam_test.go | 44 ++++++++++++++++++++- api/server.go | 4 +- cmd/server/main.go | 3 +- 5 files changed, 145 insertions(+), 6 deletions(-) diff --git a/api/errors.go b/api/errors.go index 0c36d8bb..9268516a 100644 --- a/api/errors.go +++ b/api/errors.go @@ -8,6 +8,7 @@ import ( var ( errNoCookie = errorResponse{status: http.StatusUnauthorized, message: "Cookie not found."} errSessionNotFound = errorResponse{status: http.StatusNotFound, message: "Session not found."} + errSessionExists = errorResponse{status: http.StatusNotFound, message: "Session already exists."} ) func handlerError(w http.ResponseWriter, err error) { diff --git a/api/jam.go b/api/jam.go index f4c8abac..e80b4ad2 100644 --- a/api/jam.go +++ b/api/jam.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "fmt" "io" "log" "net/http" @@ -68,6 +69,12 @@ type ( } ) +func NewJamService() *JamService { + return &JamService{ + sessions: make(map[string]*jamSession), + } +} + // new session handler func (s *JamService) NewSession(w http.ResponseWriter, r *http.Request) { // get values from the request @@ -164,6 +171,89 @@ func (s *JamService) Connect(w http.ResponseWriter, r *http.Request) { }() } +func (s *JamService) Join(w http.ResponseWriter, r *http.Request) { + // upgrade http connection to websocket + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + log.Println("connect", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + jamId := chi.URLParam(r, "jamId") + log.Println("jamId***", jamId) + + // Hardcoded new session for now + session := &jamSession{ + conns: make(map[string]*jamConn), + out: make(chan []interface{}), + + id: jamId, + name: "Jam On It!", + tempo: 85, + owner: uuid.NewString(), + } + + // Since the session is hardcoded, it probably already exists. + err = s.addSession(session) + // If the session does exist, we can ignore and keep rolling. + if err != nil && err != &errSessionExists { + handlerError(w, err) + return + } + + session, err = s.getSession(jamId) + if err != nil { + handlerError(w, err) + return + } + + go func() { + defer conn.Close() + var ( + fr = wsutil.NewReader(conn, ws.StateServerSide) + fw = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText) + decoder = json.NewDecoder(fr) + encoder = json.NewEncoder(fw) + ) + for { + hdr, err := fr.NextFrame() + if err != nil { + log.Println("NextFrame error", err) + } + if hdr.OpCode == ws.OpClose { + // Break out of for and close the connection + return + } + + // TODO: Hand off messages to some Jam level message handler + // Ex: NOTE_ON, NOTE_OFF, PLAYER_JOINED, PLAYER_MESSAGE + + // Should the message handler do the encoding/decoding since they will know which concrete type to decode into? + // TODO: Move all out to own handler + var req wsReq + if err := decoder.Decode(&req); err != nil { + log.Println(err) + } + + msg := fmt.Sprintf("Welcome to Jam %s!", session.id) + log.Println(msg) + // TODO: Create proper response types + type Resp struct { + MessageText string `json:"messageText"` + } + + resp := Resp{MessageText: msg} + if err := encoder.Encode(&resp); err != nil { + log.Println(err) + } + if err = fw.Flush(); err != nil { + log.Println(err) + } + } + }() +} + // join session handler func (s *JamService) JoinSession(w http.ResponseWriter, r *http.Request) { // get values from the request @@ -206,15 +296,20 @@ func (s *JamService) JoinSession(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -func (s *JamService) addSession(js *jamSession) { +func (s *JamService) addSession(js *jamSession) error { s.mu.Lock() + _, err := s.getSession(js.id) + if err != nil && err != &errSessionNotFound { + return &errSessionExists + } s.sessions[js.id] = js s.mu.Unlock() + return nil } func (s *JamService) getSession(sID string) (*jamSession, error) { session, ok := s.sessions[sID] - if ok { + if !ok { return &jamSession{}, &errSessionNotFound } diff --git a/api/jam_test.go b/api/jam_test.go index de71927c..cd4850c0 100644 --- a/api/jam_test.go +++ b/api/jam_test.go @@ -14,7 +14,8 @@ import ( func TestConnect(t *testing.T) { t.Run("we get a list of available Jam Session when we connect over websocket", func(t *testing.T) { - server := httptest.NewServer(NewServer().Router) + js := NewJamService() + server := httptest.NewServer(NewServer(js).Router) wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws/v1/jam" @@ -45,6 +46,47 @@ func TestConnect(t *testing.T) { }) } +func TestJamConnect(t *testing.T) { + t.Run("we receive a welcome message when entering a Jam", func(t *testing.T) { + // Hardcoding the Jam ID for now. + // In the future we'll connect through a "JAM_JOIN" message sequence. + // An empty payload denotes the client wants to join any available Jam. + // Req: { "messageType": "JAM_JOIN", "payload": {} } → server + // Response: client ← { "jamId": "123456789" } + // Client makes a new ws request @ /ws/v1/jam/123456789 + jamId := "123456789" + + js := NewJamService() + server := httptest.NewServer(NewServer(js).Router) + wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws/v1/jam/" + jamId + + conn, _, _, err := ws.DefaultDialer.Dial(context.Background(), wsURL) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + defer server.Close() + defer conn.Close() + + listJamsReq := []byte(`{"messageType": "JAM_HELLO"}`) + _, err = conn.Write(listJamsReq) + if err != nil { + t.Fatal("could not write to WS connection") + } + + within(t, time.Millisecond*10, func() { + msg, err := wsutil.ReadServerText(conn) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + want := `{"messageText":"Welcome to Jam 123456789!"}` + "\n" + if string(msg) != want { + t.Errorf(`got "%s", want "%s"`, string(msg), want) + } + }) + }) +} + func within(t testing.TB, d time.Duration, assert func()) { t.Helper() diff --git a/api/server.go b/api/server.go index 96d46350..8116218d 100644 --- a/api/server.go +++ b/api/server.go @@ -66,8 +66,7 @@ func (s *Server) ServeHTTP() { } } -func NewServer() *Server { - jamService := JamService{} +func NewServer(jamService *JamService) *Server { s := new(Server) s.Router = chi.NewMux() @@ -75,6 +74,7 @@ func NewServer() *Server { s.Router.Route("/ws/v1", func(r chi.Router) { r.Use(middleware.Logger) r.Get("/jam", jamService.Connect) + r.Get("/jam/{jamId}", jamService.Join) }) return s diff --git a/cmd/server/main.go b/cmd/server/main.go index dd575014..3f0324e9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -15,7 +15,8 @@ func main() { log.Fatalf("failed to read config: %v", err.Error()) } - server := api.NewServer() + js := api.NewJamService() + server := api.NewServer(js) port := ":" + viper.GetString("PORT") log.Printf("starting the server on %s%s\n", server.Host, port) From 38efa72c779d71a93c59616f00dfcf23e9d86a85 Mon Sep 17 00:00:00 2001 From: adoublef Date: Wed, 14 Sep 2022 21:50:56 +0100 Subject: [PATCH 014/246] single pool with connections --- api/errors.go | 24 --- api/jam.go | 372 --------------------------------------------- api/jam_test.go | 105 ------------- api/server.go | 81 ---------- api/utils.go | 31 ---- cmd/cli/main.go | 8 - cmd/server/main.go | 25 --- 7 files changed, 646 deletions(-) delete mode 100644 api/errors.go delete mode 100644 api/jam.go delete mode 100644 api/jam_test.go delete mode 100644 api/server.go delete mode 100644 api/utils.go delete mode 100644 cmd/cli/main.go delete mode 100644 cmd/server/main.go diff --git a/api/errors.go b/api/errors.go deleted file mode 100644 index 9268516a..00000000 --- a/api/errors.go +++ /dev/null @@ -1,24 +0,0 @@ -package api - -import ( - "log" - "net/http" -) - -var ( - errNoCookie = errorResponse{status: http.StatusUnauthorized, message: "Cookie not found."} - errSessionNotFound = errorResponse{status: http.StatusNotFound, message: "Session not found."} - errSessionExists = errorResponse{status: http.StatusNotFound, message: "Session already exists."} -) - -func handlerError(w http.ResponseWriter, err error) { - if err != nil { - if httpError, ok := err.(*errorResponse); ok { - http.Error(w, httpError.message, httpError.status) - return - } - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} diff --git a/api/jam.go b/api/jam.go deleted file mode 100644 index e80b4ad2..00000000 --- a/api/jam.go +++ /dev/null @@ -1,372 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "sync" - - "github.com/go-chi/chi/v5" - "github.com/gobwas/ws" - "github.com/gobwas/ws/wsutil" - "github.com/google/uuid" -) - -// struct type to store info related to a websocket connection -type jamConn struct { - mu sync.Mutex - conn io.ReadWriter - - id string - username string -} - -// struct type to store info related to a Jam session -// also contains a map of current connections to the session -type jamSession struct { - mu sync.RWMutex - conns map[string]*jamConn - out chan []interface{} - - id string - name string - tempo uint - owner string -} - -// struct type for the Jam service -// also contains current available sessions created by users -type JamService struct { - mu sync.RWMutex - sessions map[string]*jamSession -} - -// request types -type ( - newJamReq struct { - Username string `json:"username"` - SessionName string `json:"session_name"` - Tempo uint `json:"tempo"` - } - - joinJamReq struct { - Username string `json:"username"` - SessionID string `json:"session_id"` - } - wsReq struct { - MessageType string `json:"messageType"` // Type of application message. Ex: JAM_SESSION_CONNECT, JAM_SESSION_CREATE - Payload struct{} `json:"payload"` // Payload of message, differs according to MessageType. Ex: JAM_SESSION_CREATE may contain Jam Session config (tempo, session name, etc). - } - - JamSlim struct { - Id string `json:"id"` - Name string `json:"name"` - } - listJamsResp struct { - Jams []JamSlim - } -) - -func NewJamService() *JamService { - return &JamService{ - sessions: make(map[string]*jamSession), - } -} - -// new session handler -func (s *JamService) NewSession(w http.ResponseWriter, r *http.Request) { - // get values from the request - si := newJamReq{} - if err := parse(r, &si); err != nil { - log.Println(err) - w.WriteHeader(http.StatusBadRequest) - return - } - - // upgrade http connection to websocket - conn, _, _, err := ws.UpgradeHTTP(r, w) - if err != nil { - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // create a new connection between the owner and the server - // then add it to the session connections - c := jamConn{} - c.conn = conn - c.id = uuid.NewString() - if isEmptyString(si.Username) { - c.username = c.id - } else { - c.username = si.Username - } - - // create a new session and set the session owner - session := jamSession{ - conns: make(map[string]*jamConn), - out: make(chan []interface{}), - - id: uuid.NewString(), - name: si.SessionName, - tempo: si.Tempo, - owner: c.id, - } - // add session to sessions map - s.addSession(&session) - // check for errors - // err isn't nil if the username is already used - session.addConn(&c) - session.broadcast("Welcome to Rapidmidiex!") - - w.WriteHeader(http.StatusOK) -} - -// Connect establishes a WebSocket connection with the application. From there we can communicate with the client about which session to join. -func (s *JamService) Connect(w http.ResponseWriter, r *http.Request) { - // upgrade http connection to websocket - conn, _, _, err := ws.UpgradeHTTP(r, w) - if err != nil { - log.Println("connect", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - go func() { - defer conn.Close() - var ( - fr = wsutil.NewReader(conn, ws.StateServerSide) - fw = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText) - decoder = json.NewDecoder(fr) - encoder = json.NewEncoder(fw) - ) - for { - hdr, err := fr.NextFrame() - if err != nil { - log.Println(err) - } - if hdr.OpCode == ws.OpClose { - log.Println(io.EOF) - // Break out of for and close the connection - return - } - - // TODO: Hand off messages to some app level message broker - // Ex: JAM_SESSION_CONNECT, JAM_SESSION_CREATE - var req wsReq - if err := decoder.Decode(&req); err != nil { - log.Println(err) - } - - resp := listJamsResp{Jams: make([]JamSlim, 0)} - if err := encoder.Encode(&resp); err != nil { - log.Println(err) - } - if err = fw.Flush(); err != nil { - log.Println(err) - } - } - }() -} - -func (s *JamService) Join(w http.ResponseWriter, r *http.Request) { - // upgrade http connection to websocket - conn, _, _, err := ws.UpgradeHTTP(r, w) - if err != nil { - log.Println("connect", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - jamId := chi.URLParam(r, "jamId") - log.Println("jamId***", jamId) - - // Hardcoded new session for now - session := &jamSession{ - conns: make(map[string]*jamConn), - out: make(chan []interface{}), - - id: jamId, - name: "Jam On It!", - tempo: 85, - owner: uuid.NewString(), - } - - // Since the session is hardcoded, it probably already exists. - err = s.addSession(session) - // If the session does exist, we can ignore and keep rolling. - if err != nil && err != &errSessionExists { - handlerError(w, err) - return - } - - session, err = s.getSession(jamId) - if err != nil { - handlerError(w, err) - return - } - - go func() { - defer conn.Close() - var ( - fr = wsutil.NewReader(conn, ws.StateServerSide) - fw = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText) - decoder = json.NewDecoder(fr) - encoder = json.NewEncoder(fw) - ) - for { - hdr, err := fr.NextFrame() - if err != nil { - log.Println("NextFrame error", err) - } - if hdr.OpCode == ws.OpClose { - // Break out of for and close the connection - return - } - - // TODO: Hand off messages to some Jam level message handler - // Ex: NOTE_ON, NOTE_OFF, PLAYER_JOINED, PLAYER_MESSAGE - - // Should the message handler do the encoding/decoding since they will know which concrete type to decode into? - // TODO: Move all out to own handler - var req wsReq - if err := decoder.Decode(&req); err != nil { - log.Println(err) - } - - msg := fmt.Sprintf("Welcome to Jam %s!", session.id) - log.Println(msg) - // TODO: Create proper response types - type Resp struct { - MessageText string `json:"messageText"` - } - - resp := Resp{MessageText: msg} - if err := encoder.Encode(&resp); err != nil { - log.Println(err) - } - if err = fw.Flush(); err != nil { - log.Println(err) - } - } - }() -} - -// join session handler -func (s *JamService) JoinSession(w http.ResponseWriter, r *http.Request) { - // get values from the request - ji := joinJamReq{} - if err := parse(r, &ji); err != nil { - log.Println(err) - w.WriteHeader(http.StatusBadRequest) - return - } - - sID := chi.URLParam(r, "session_id") - - // err isn't nil if session doesn't exist - session, err := s.getSession(sID) - if err != nil { - handlerError(w, err) - return - } - - // upgrade http connection to websocket - conn, _, _, err := ws.UpgradeHTTP(r, w) - if err != nil { - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // create a new connection - c := jamConn{} - c.conn = conn - c.id = uuid.NewString() - if isEmptyString(ji.Username) { - c.username = c.id - } else { - c.username = ji.Username - } - session.addConn(&c) - session.broadcast("Welcome " + ji.Username + "!") - - w.WriteHeader(http.StatusOK) -} - -func (s *JamService) addSession(js *jamSession) error { - s.mu.Lock() - _, err := s.getSession(js.id) - if err != nil && err != &errSessionNotFound { - return &errSessionExists - } - s.sessions[js.id] = js - s.mu.Unlock() - return nil -} - -func (s *JamService) getSession(sID string) (*jamSession, error) { - session, ok := s.sessions[sID] - if !ok { - return &jamSession{}, &errSessionNotFound - } - - return session, nil -} - -func (s *jamSession) addConn(jc *jamConn) { - s.mu.Lock() - s.conns[jc.id] = jc - s.mu.Unlock() -} - -func (c *jamConn) write(i interface{}) error { - w := wsutil.NewWriter(c.conn, ws.StateServerSide, ws.OpText) - encoder := json.NewEncoder(w) - - c.mu.Lock() - defer c.mu.Unlock() - - if err := encoder.Encode(i); err != nil { - return err - } - - return w.Flush() -} - -func (c *jamConn) writeRaw(b []byte) error { - c.mu.Lock() - defer c.mu.Unlock() - - _, err := c.conn.Write(b) - return err -} - -// func (s *jamSession) writer(i interface{}) { -// for bts := range c.out { -// s.mu.RLock() -// cs := s.conns -// s.mu.RUnlock() -// -// for _, c := range cs { -// c := c // For closure. -// c.writeRaw(bts) -// } -// } -// } - -// iterates through session connections -// and send provided message to each of them -func (s *jamSession) broadcast(i interface{}) { - for _, c := range s.conns { - select { - case <-s.out: - c.write(i) - default: - delete(s.conns, c.username) - // c.close() - } - } -} diff --git a/api/jam_test.go b/api/jam_test.go deleted file mode 100644 index cd4850c0..00000000 --- a/api/jam_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package api - -import ( - "context" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/gobwas/ws" - "github.com/gobwas/ws/wsutil" -) - -func TestConnect(t *testing.T) { - t.Run("we get a list of available Jam Session when we connect over websocket", func(t *testing.T) { - - js := NewJamService() - server := httptest.NewServer(NewServer(js).Router) - - wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws/v1/jam" - - conn, _, _, err := ws.DefaultDialer.Dial(context.Background(), wsURL) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - defer server.Close() - defer conn.Close() - - listJamsReq := []byte(`{"messageType": "JAM_LIST"}`) - _, err = conn.Write(listJamsReq) - if err != nil { - t.Fatal("could not write to WS connection") - } - - within(t, time.Millisecond*10, func() { - msg, err := wsutil.ReadServerText(conn) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - want := `{"Jams":[]}` + "\n" - if string(msg) != want { - t.Errorf(`got "%s", want "%s"`, string(msg), want) - } - }) - }) -} - -func TestJamConnect(t *testing.T) { - t.Run("we receive a welcome message when entering a Jam", func(t *testing.T) { - // Hardcoding the Jam ID for now. - // In the future we'll connect through a "JAM_JOIN" message sequence. - // An empty payload denotes the client wants to join any available Jam. - // Req: { "messageType": "JAM_JOIN", "payload": {} } → server - // Response: client ← { "jamId": "123456789" } - // Client makes a new ws request @ /ws/v1/jam/123456789 - jamId := "123456789" - - js := NewJamService() - server := httptest.NewServer(NewServer(js).Router) - wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws/v1/jam/" + jamId - - conn, _, _, err := ws.DefaultDialer.Dial(context.Background(), wsURL) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - defer server.Close() - defer conn.Close() - - listJamsReq := []byte(`{"messageType": "JAM_HELLO"}`) - _, err = conn.Write(listJamsReq) - if err != nil { - t.Fatal("could not write to WS connection") - } - - within(t, time.Millisecond*10, func() { - msg, err := wsutil.ReadServerText(conn) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - want := `{"messageText":"Welcome to Jam 123456789!"}` + "\n" - if string(msg) != want { - t.Errorf(`got "%s", want "%s"`, string(msg), want) - } - }) - }) -} - -func within(t testing.TB, d time.Duration, assert func()) { - t.Helper() - - done := make(chan struct{}, 1) - - go func() { - assert() - done <- struct{}{} - }() - - select { - case <-time.After(d): - t.Error("timed out") - case <-done: - } -} diff --git a/api/server.go b/api/server.go deleted file mode 100644 index 8116218d..00000000 --- a/api/server.go +++ /dev/null @@ -1,81 +0,0 @@ -package api - -import ( - "context" - "log" - "net" - "net/http" - "os/signal" - "syscall" - "time" - - "github.com/go-chi/chi/middleware" - "github.com/go-chi/chi/v5" - "github.com/rs/cors" - "golang.org/x/sync/errgroup" -) - -type Server struct { - Host string - Port string - Router *chi.Mux -} - -func (s *Server) ServeHTTP() { - // "*" shouldn't be used as AllowedOrigins - c := cors.Options{ - AllowedOrigins: []string{"http://localhost:3000", "http://127.0.0.1:3000"}, - AllowCredentials: true, - AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodPatch}, - AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, - } - h := cors.New(c).Handler(s.Router) - - serverCtx, serverStop := signal.NotifyContext( - context.Background(), - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - defer serverStop() - - server := http.Server{ - Addr: s.Port, - Handler: h, - ReadTimeout: 10 * time.Second, // max time to read request from the client - WriteTimeout: 10 * time.Second, // max time to write response to the client - IdleTimeout: 120 * time.Second, // max time for connections using TCP Keep-Alive - BaseContext: func(_ net.Listener) context.Context { - return serverCtx - }, - } - - g, gCtx := errgroup.WithContext(serverCtx) - g.Go(func() error { - // Run the server - return server.ListenAndServe() - }) - g.Go(func() error { - <-gCtx.Done() - return server.Shutdown(context.Background()) - }) - - if err := g.Wait(); err != nil { - log.Printf("exit reason: %s \n", err) - } -} - -func NewServer(jamService *JamService) *Server { - s := new(Server) - - s.Router = chi.NewMux() - - s.Router.Route("/ws/v1", func(r chi.Router) { - r.Use(middleware.Logger) - r.Get("/jam", jamService.Connect) - r.Get("/jam/{jamId}", jamService.Join) - }) - - return s -} diff --git a/api/utils.go b/api/utils.go deleted file mode 100644 index b12df655..00000000 --- a/api/utils.go +++ /dev/null @@ -1,31 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - "strings" -) - -func isEmptyString(s string) bool { - return len(strings.TrimSpace(s)) == 0 -} - -func parse(r *http.Request, out interface{}) error { - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&out) - if err != nil { - return err - } - - return nil -} - -type errorResponse struct { - status int - message string -} - -// custom error type for detecting internal application errors -func (e *errorResponse) Error() string { - return e.message -} diff --git a/cmd/cli/main.go b/cmd/cli/main.go deleted file mode 100644 index 63e4dc31..00000000 --- a/cmd/cli/main.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -import "fmt" - -func main() { - // Feel free to delete this file. - fmt.Println("Hello Gophers") -} diff --git a/cmd/server/main.go b/cmd/server/main.go deleted file mode 100644 index 3f0324e9..00000000 --- a/cmd/server/main.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "log" - "net/http" - - rmx "github.com/rog-golang-buddies/rapidmidiex" - "github.com/rog-golang-buddies/rapidmidiex/api" - "github.com/spf13/viper" -) - -func main() { - err := rmx.LoadConfig() - if err != nil { - log.Fatalf("failed to read config: %v", err.Error()) - } - - js := api.NewJamService() - server := api.NewServer(js) - - port := ":" + viper.GetString("PORT") - log.Printf("starting the server on %s%s\n", server.Host, port) - - log.Fatal(http.ListenAndServe(port, server.Router)) -} From 3e5671e7a2aada2fe25df690d76083c84045208a Mon Sep 17 00:00:00 2001 From: adoublef Date: Wed, 14 Sep 2022 21:51:39 +0100 Subject: [PATCH 015/246] single pool with connections --- assets/src/index.js | 26 +++ cli/main.go | 8 + cmd/main.go | 15 ++ internal/errors.go | 24 +++ internal/jam.go | 372 +++++++++++++++++++++++++++++++++++++++++++ internal/jam_test.go | 105 ++++++++++++ internal/server.go | 81 ++++++++++ internal/utils.go | 31 ++++ pages/index.html | 16 ++ www/middleware.go | 34 ++++ www/routes.go | 46 ++++++ www/service.go | 35 ++++ www/ws/conn.go | 24 +++ www/ws/pool.go | 83 ++++++++++ www/www.go | 18 +++ 15 files changed, 918 insertions(+) create mode 100644 assets/src/index.js create mode 100644 cli/main.go create mode 100644 cmd/main.go create mode 100644 internal/errors.go create mode 100644 internal/jam.go create mode 100644 internal/jam_test.go create mode 100644 internal/server.go create mode 100644 internal/utils.go create mode 100644 pages/index.html create mode 100644 www/middleware.go create mode 100644 www/routes.go create mode 100644 www/service.go create mode 100644 www/ws/conn.go create mode 100644 www/ws/pool.go create mode 100644 www/www.go diff --git a/assets/src/index.js b/assets/src/index.js new file mode 100644 index 00000000..c4136e5f --- /dev/null +++ b/assets/src/index.js @@ -0,0 +1,26 @@ +const app = document.getElementById("app"); + +app.innerHTML = ` +

This is a dynamic page

+ +`; + +const ws = new WebSocket(`ws://localhost:8888/jam`); + +ws.addEventListener("open", e => { + alert("web socket has opened"); +}); + +ws.addEventListener("message", e => { + alert(e.data); +}); + +ws.addEventListener("error", e => { + console.error(e); + + document.querySelector("button").disabled = true; +}); + +document.querySelector("button").addEventListener("click", e => { + ws.send(1); +}); \ No newline at end of file diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 00000000..63e4dc31 --- /dev/null +++ b/cli/main.go @@ -0,0 +1,8 @@ +package main + +import "fmt" + +func main() { + // Feel free to delete this file. + fmt.Println("Hello Gophers") +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 00000000..4fcd1829 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "log" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/rog-golang-buddies/rapidmidiex/www" +) + +func main() { + s := www.NewService(chi.NewMux()) + log.Println("http://localhost:8888") + log.Fatalln(http.ListenAndServe(":8888", s)) +} diff --git a/internal/errors.go b/internal/errors.go new file mode 100644 index 00000000..92d7e076 --- /dev/null +++ b/internal/errors.go @@ -0,0 +1,24 @@ +package internal + +import ( + "log" + "net/http" +) + +var ( + errNoCookie = errorResponse{status: http.StatusUnauthorized, message: "Cookie not found."} + errSessionNotFound = errorResponse{status: http.StatusNotFound, message: "Session not found."} + errSessionExists = errorResponse{status: http.StatusNotFound, message: "Session already exists."} +) + +func handlerError(w http.ResponseWriter, err error) { + if err != nil { + if httpError, ok := err.(*errorResponse); ok { + http.Error(w, httpError.message, httpError.status) + return + } + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } +} diff --git a/internal/jam.go b/internal/jam.go new file mode 100644 index 00000000..b06706dc --- /dev/null +++ b/internal/jam.go @@ -0,0 +1,372 @@ +package internal + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "sync" + + "github.com/go-chi/chi/v5" + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" + "github.com/google/uuid" +) + +// struct type to store info related to a websocket connection +type jamConn struct { + mu sync.Mutex + conn io.ReadWriter + + id string + username string +} + +// struct type to store info related to a Jam session +// also contains a map of current connections to the session +type jamSession struct { + mu sync.RWMutex + conns map[string]*jamConn + out chan []interface{} + + id string + name string + tempo uint + owner string +} + +// struct type for the Jam service +// also contains current available sessions created by users +type JamService struct { + mu sync.RWMutex + sessions map[string]*jamSession +} + +// request types +type ( + newJamReq struct { + Username string `json:"username"` + SessionName string `json:"session_name"` + Tempo uint `json:"tempo"` + } + + joinJamReq struct { + Username string `json:"username"` + SessionID string `json:"session_id"` + } + wsReq struct { + MessageType string `json:"messageType"` // Type of application message. Ex: JAM_SESSION_CONNECT, JAM_SESSION_CREATE + Payload struct{} `json:"payload"` // Payload of message, differs according to MessageType. Ex: JAM_SESSION_CREATE may contain Jam Session config (tempo, session name, etc). + } + + JamSlim struct { + Id string `json:"id"` + Name string `json:"name"` + } + listJamsResp struct { + Jams []JamSlim + } +) + +func NewJamService() *JamService { + return &JamService{ + sessions: make(map[string]*jamSession), + } +} + +// new session handler +func (s *JamService) NewSession(w http.ResponseWriter, r *http.Request) { + // get values from the request + si := newJamReq{} + if err := parse(r, &si); err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + return + } + + // upgrade http connection to websocket + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // create a new connection between the owner and the server + // then add it to the session connections + c := jamConn{} + c.conn = conn + c.id = uuid.NewString() + if isEmptyString(si.Username) { + c.username = c.id + } else { + c.username = si.Username + } + + // create a new session and set the session owner + session := jamSession{ + conns: make(map[string]*jamConn), + out: make(chan []interface{}), + + id: uuid.NewString(), + name: si.SessionName, + tempo: si.Tempo, + owner: c.id, + } + // add session to sessions map + s.addSession(&session) + // check for errors + // err isn't nil if the username is already used + session.addConn(&c) + session.broadcast("Welcome to Rapidmidiex!") + + w.WriteHeader(http.StatusOK) +} + +// Connect establishes a WebSocket connection with the application. From there we can communicate with the client about which session to join. +func (s *JamService) Connect(w http.ResponseWriter, r *http.Request) { + // upgrade http connection to websocket + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + log.Println("connect", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + go func() { + defer conn.Close() + var ( + fr = wsutil.NewReader(conn, ws.StateServerSide) + fw = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText) + decoder = json.NewDecoder(fr) + encoder = json.NewEncoder(fw) + ) + for { + hdr, err := fr.NextFrame() + if err != nil { + log.Println(err) + } + if hdr.OpCode == ws.OpClose { + log.Println(io.EOF) + // Break out of for and close the connection + return + } + + // TODO: Hand off messages to some app level message broker + // Ex: JAM_SESSION_CONNECT, JAM_SESSION_CREATE + var req wsReq + if err := decoder.Decode(&req); err != nil { + log.Println(err) + } + + resp := listJamsResp{Jams: make([]JamSlim, 0)} + if err := encoder.Encode(&resp); err != nil { + log.Println(err) + } + if err = fw.Flush(); err != nil { + log.Println(err) + } + } + }() +} + +func (s *JamService) Join(w http.ResponseWriter, r *http.Request) { + // upgrade http connection to websocket + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + log.Println("connect", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + jamId := chi.URLParam(r, "jamId") + log.Println("jamId***", jamId) + + // Hardcoded new session for now + session := &jamSession{ + conns: make(map[string]*jamConn), + out: make(chan []interface{}), + + id: jamId, + name: "Jam On It!", + tempo: 85, + owner: uuid.NewString(), + } + + // Since the session is hardcoded, it probably already exists. + err = s.addSession(session) + // If the session does exist, we can ignore and keep rolling. + if err != nil && err != &errSessionExists { + handlerError(w, err) + return + } + + session, err = s.getSession(jamId) + if err != nil { + handlerError(w, err) + return + } + + go func() { + defer conn.Close() + var ( + fr = wsutil.NewReader(conn, ws.StateServerSide) + fw = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText) + decoder = json.NewDecoder(fr) + encoder = json.NewEncoder(fw) + ) + for { + hdr, err := fr.NextFrame() + if err != nil { + log.Println("NextFrame error", err) + } + if hdr.OpCode == ws.OpClose { + // Break out of for and close the connection + return + } + + // TODO: Hand off messages to some Jam level message handler + // Ex: NOTE_ON, NOTE_OFF, PLAYER_JOINED, PLAYER_MESSAGE + + // Should the message handler do the encoding/decoding since they will know which concrete type to decode into? + // TODO: Move all out to own handler + var req wsReq + if err := decoder.Decode(&req); err != nil { + log.Println(err) + } + + msg := fmt.Sprintf("Welcome to Jam %s!", session.id) + log.Println(msg) + // TODO: Create proper response types + type Resp struct { + MessageText string `json:"messageText"` + } + + resp := Resp{MessageText: msg} + if err := encoder.Encode(&resp); err != nil { + log.Println(err) + } + if err = fw.Flush(); err != nil { + log.Println(err) + } + } + }() +} + +// join session handler +func (s *JamService) JoinSession(w http.ResponseWriter, r *http.Request) { + // get values from the request + ji := joinJamReq{} + if err := parse(r, &ji); err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + return + } + + sID := chi.URLParam(r, "session_id") + + // err isn't nil if session doesn't exist + session, err := s.getSession(sID) + if err != nil { + handlerError(w, err) + return + } + + // upgrade http connection to websocket + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // create a new connection + c := jamConn{} + c.conn = conn + c.id = uuid.NewString() + if isEmptyString(ji.Username) { + c.username = c.id + } else { + c.username = ji.Username + } + session.addConn(&c) + session.broadcast("Welcome " + ji.Username + "!") + + w.WriteHeader(http.StatusOK) +} + +func (s *JamService) addSession(js *jamSession) error { + s.mu.Lock() + _, err := s.getSession(js.id) + if err != nil && err != &errSessionNotFound { + return &errSessionExists + } + s.sessions[js.id] = js + s.mu.Unlock() + return nil +} + +func (s *JamService) getSession(sID string) (*jamSession, error) { + session, ok := s.sessions[sID] + if !ok { + return &jamSession{}, &errSessionNotFound + } + + return session, nil +} + +func (s *jamSession) addConn(jc *jamConn) { + s.mu.Lock() + s.conns[jc.id] = jc + s.mu.Unlock() +} + +func (c *jamConn) write(i interface{}) error { + w := wsutil.NewWriter(c.conn, ws.StateServerSide, ws.OpText) + encoder := json.NewEncoder(w) + + c.mu.Lock() + defer c.mu.Unlock() + + if err := encoder.Encode(i); err != nil { + return err + } + + return w.Flush() +} + +func (c *jamConn) writeRaw(b []byte) error { + c.mu.Lock() + defer c.mu.Unlock() + + _, err := c.conn.Write(b) + return err +} + +// func (s *jamSession) writer(i interface{}) { +// for bts := range c.out { +// s.mu.RLock() +// cs := s.conns +// s.mu.RUnlock() +// +// for _, c := range cs { +// c := c // For closure. +// c.writeRaw(bts) +// } +// } +// } + +// iterates through session connections +// and send provided message to each of them +func (s *jamSession) broadcast(i interface{}) { + for _, c := range s.conns { + select { + case <-s.out: + c.write(i) + default: + delete(s.conns, c.username) + // c.close() + } + } +} diff --git a/internal/jam_test.go b/internal/jam_test.go new file mode 100644 index 00000000..36ab509d --- /dev/null +++ b/internal/jam_test.go @@ -0,0 +1,105 @@ +package internal + +import ( + "context" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" +) + +func TestConnect(t *testing.T) { + t.Run("we get a list of available Jam Session when we connect over websocket", func(t *testing.T) { + + js := NewJamService() + server := httptest.NewServer(NewServer(js).Router) + + wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws/v1/jam" + + conn, _, _, err := ws.DefaultDialer.Dial(context.Background(), wsURL) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + defer server.Close() + defer conn.Close() + + listJamsReq := []byte(`{"messageType": "JAM_LIST"}`) + _, err = conn.Write(listJamsReq) + if err != nil { + t.Fatal("could not write to WS connection") + } + + within(t, time.Millisecond*10, func() { + msg, err := wsutil.ReadServerText(conn) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + want := `{"Jams":[]}` + "\n" + if string(msg) != want { + t.Errorf(`got "%s", want "%s"`, string(msg), want) + } + }) + }) +} + +func TestJamConnect(t *testing.T) { + t.Run("we receive a welcome message when entering a Jam", func(t *testing.T) { + // Hardcoding the Jam ID for now. + // In the future we'll connect through a "JAM_JOIN" message sequence. + // An empty payload denotes the client wants to join any available Jam. + // Req: { "messageType": "JAM_JOIN", "payload": {} } → server + // Response: client ← { "jamId": "123456789" } + // Client makes a new ws request @ /ws/v1/jam/123456789 + jamId := "123456789" + + js := NewJamService() + server := httptest.NewServer(NewServer(js).Router) + wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws/v1/jam/" + jamId + + conn, _, _, err := ws.DefaultDialer.Dial(context.Background(), wsURL) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + defer server.Close() + defer conn.Close() + + listJamsReq := []byte(`{"messageType": "JAM_HELLO"}`) + _, err = conn.Write(listJamsReq) + if err != nil { + t.Fatal("could not write to WS connection") + } + + within(t, time.Millisecond*10, func() { + msg, err := wsutil.ReadServerText(conn) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + want := `{"messageText":"Welcome to Jam 123456789!"}` + "\n" + if string(msg) != want { + t.Errorf(`got "%s", want "%s"`, string(msg), want) + } + }) + }) +} + +func within(t testing.TB, d time.Duration, assert func()) { + t.Helper() + + done := make(chan struct{}, 1) + + go func() { + assert() + done <- struct{}{} + }() + + select { + case <-time.After(d): + t.Error("timed out") + case <-done: + } +} diff --git a/internal/server.go b/internal/server.go new file mode 100644 index 00000000..b7366025 --- /dev/null +++ b/internal/server.go @@ -0,0 +1,81 @@ +package internal + +import ( + "context" + "log" + "net" + "net/http" + "os/signal" + "syscall" + "time" + + "github.com/go-chi/chi/middleware" + "github.com/go-chi/chi/v5" + "github.com/rs/cors" + "golang.org/x/sync/errgroup" +) + +type Server struct { + Host string + Port string + Router *chi.Mux +} + +func (s *Server) ServeHTTP() { + // "*" shouldn't be used as AllowedOrigins + c := cors.Options{ + AllowedOrigins: []string{"http://localhost:3000", "http://127.0.0.1:3000"}, + AllowCredentials: true, + AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodPatch}, + AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + } + h := cors.New(c).Handler(s.Router) + + serverCtx, serverStop := signal.NotifyContext( + context.Background(), + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + defer serverStop() + + server := http.Server{ + Addr: s.Port, + Handler: h, + ReadTimeout: 10 * time.Second, // max time to read request from the client + WriteTimeout: 10 * time.Second, // max time to write response to the client + IdleTimeout: 120 * time.Second, // max time for connections using TCP Keep-Alive + BaseContext: func(_ net.Listener) context.Context { + return serverCtx + }, + } + + g, gCtx := errgroup.WithContext(serverCtx) + g.Go(func() error { + // Run the server + return server.ListenAndServe() + }) + g.Go(func() error { + <-gCtx.Done() + return server.Shutdown(context.Background()) + }) + + if err := g.Wait(); err != nil { + log.Printf("exit reason: %s \n", err) + } +} + +func NewServer(jamService *JamService) *Server { + s := new(Server) + + s.Router = chi.NewMux() + + s.Router.Route("/ws/v1", func(r chi.Router) { + r.Use(middleware.Logger) + r.Get("/jam", jamService.Connect) + r.Get("/jam/{jamId}", jamService.Join) + }) + + return s +} diff --git a/internal/utils.go b/internal/utils.go new file mode 100644 index 00000000..a326a6f0 --- /dev/null +++ b/internal/utils.go @@ -0,0 +1,31 @@ +package internal + +import ( + "encoding/json" + "net/http" + "strings" +) + +func isEmptyString(s string) bool { + return len(strings.TrimSpace(s)) == 0 +} + +func parse(r *http.Request, out interface{}) error { + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&out) + if err != nil { + return err + } + + return nil +} + +type errorResponse struct { + status int + message string +} + +// custom error type for detecting internal application errors +func (e *errorResponse) Error() string { + return e.message +} diff --git a/pages/index.html b/pages/index.html new file mode 100644 index 00000000..2ac5de0a --- /dev/null +++ b/pages/index.html @@ -0,0 +1,16 @@ + + + + + + + + Home + + + + +
+ + + \ No newline at end of file diff --git a/www/middleware.go b/www/middleware.go new file mode 100644 index 00000000..0eb46cef --- /dev/null +++ b/www/middleware.go @@ -0,0 +1,34 @@ +package www + +import ( + "context" + "fmt" + "net/http" + + "github.com/gorilla/websocket" +) + +func (s Service) upgradeHTTP(f http.HandlerFunc) http.HandlerFunc { + u := &websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { return true }, + } + + return func(w http.ResponseWriter, r *http.Request) { + if s.p.Size() == s.p.MaxConn { + err := fmt.Errorf("pool: maximum number of connections reached") + s.respond(w, r, err, http.StatusUnauthorized) + return + } + + c, err := s.p.NewConn(w, r, u) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + r = r.WithContext(context.WithValue(r.Context(), upgradeKey, c)) + f(w, r) + } +} diff --git a/www/routes.go b/www/routes.go new file mode 100644 index 00000000..8a7c65b9 --- /dev/null +++ b/www/routes.go @@ -0,0 +1,46 @@ +package www + +import ( + "net/http" + + t "github.com/hyphengolang/prelude/template" + "github.com/rog-golang-buddies/rapidmidiex/www/ws" +) + +func (s Service) routes() { + // http + s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) + s.r.Handle("/", s.handleIndexHTML("pages/index.html")) + + // ws + s.r.HandleFunc("/jam", chain(s.handleJamSessionWS, s.upgradeHTTP)) +} + +func (s Service) handleIndexHTML(path string) http.HandlerFunc { + render, err := t.Render(path) + if err != nil { + panic(err) + } + + return func(w http.ResponseWriter, r *http.Request) { + render(w, r, nil) + } +} + +func (s Service) handleJamSessionWS(w http.ResponseWriter, r *http.Request) { + c := r.Context().Value(upgradeKey).(*ws.Conn) + defer c.Close() + + for { + var n int + if err := c.ReadJSON(&n); err != nil { + s.l.Println(err) + return + } + + if err := c.SendMessage(n + 10); err != nil { + s.l.Println(err) + return + } + } +} diff --git a/www/service.go b/www/service.go new file mode 100644 index 00000000..d0dd5ca4 --- /dev/null +++ b/www/service.go @@ -0,0 +1,35 @@ +package www + +import ( + "log" + "net/http" + + "github.com/go-chi/chi/v5" + + h "github.com/hyphengolang/prelude/http" + + "github.com/rog-golang-buddies/rapidmidiex/www/ws" +) + +type Service struct { + r chi.Router + l *log.Logger + + p *ws.Pool +} + +func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } + +func NewService(r chi.Router) *Service { + s := &Service{r, log.Default(), ws.DefaultPool()} + s.routes() + return s +} + +func (s Service) respond(w http.ResponseWriter, r *http.Request, data interface{}, status int) { + h.Respond(w, r, data, status) +} + +func (s Service) fileServer(prefix string, dirname string) http.Handler { + return h.FileServer(prefix, dirname) +} diff --git a/www/ws/conn.go b/www/ws/conn.go new file mode 100644 index 00000000..27a63960 --- /dev/null +++ b/www/ws/conn.go @@ -0,0 +1,24 @@ +package ws + +import ( + "github.com/google/uuid" + "github.com/gorilla/websocket" +) + +type Conn struct { + ID uuid.UUID + + rwc *websocket.Conn + p *Pool +} + +func (c Conn) Close() error { + c.p.Delete(c.ID) + return c.rwc.Close() +} + +func (c Conn) ReadJSON(v any) error { return c.rwc.ReadJSON(v) } + +func (c Conn) WriteJSON(v any) error { return c.rwc.WriteJSON(v) } + +func (c Conn) SendMessage(v any) error { c.p.msgs <- v; return nil } diff --git a/www/ws/pool.go b/www/ws/pool.go new file mode 100644 index 00000000..1b977166 --- /dev/null +++ b/www/ws/pool.go @@ -0,0 +1,83 @@ +package ws + +import ( + "net/http" + "sync" + + "github.com/google/uuid" + "github.com/gorilla/websocket" +) + +type Pool struct { + mu sync.Mutex + + ID uuid.UUID + MaxConn int + + cs map[uuid.UUID]*Conn + msgs chan any +} + +func DefaultPool() *Pool { + p := &Pool{ + ID: uuid.New(), + MaxConn: 2, + cs: make(map[uuid.UUID]*Conn), + msgs: make(chan any), + } + + go func() { + defer p.Close() + + for msg := range p.msgs { + for _, c := range p.cs { + c.WriteJSON(msg) + } + } + }() + + return p +} + +func (p *Pool) Size() int { + p.mu.Lock() + defer p.mu.Unlock() + + return len(p.cs) +} + +func (p *Pool) NewConn(w http.ResponseWriter, r *http.Request, u *websocket.Upgrader) (*Conn, error) { + p.mu.Lock() + defer p.mu.Unlock() + + rwc, err := u.Upgrade(w, r, nil) + if err != nil { + return nil, err + } + + c := &Conn{uuid.New(), rwc, p} + + p.cs[c.ID] = c + + return c, nil +} + +func (p *Pool) Delete(uid uuid.UUID) { + p.mu.Lock() + defer p.mu.Unlock() + + delete(p.cs, uid) +} + +func (p *Pool) Close() error { + p.mu.Lock() + defer p.mu.Unlock() + + for _, c := range p.cs { + if err := c.Close(); err != nil { + return err + } + } + + return nil +} diff --git a/www/www.go b/www/www.go new file mode 100644 index 00000000..2b491c13 --- /dev/null +++ b/www/www.go @@ -0,0 +1,18 @@ +package www + +import ( + "net/http" + + h "github.com/hyphengolang/prelude/http" +) + +type contextKey struct{ string } + +func (c *contextKey) String() string { return "context value " + c.string } + +var ( + roomKey = &contextKey{"ws-pool"} + upgradeKey = &contextKey{"http-upgrade"} +) + +func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } From 93e0c65d6630bdaaa34964fada28d73072443eb6 Mon Sep 17 00:00:00 2001 From: adoublef Date: Wed, 14 Sep 2022 23:56:51 +0100 Subject: [PATCH 016/246] todo: add RESTful endpoint to fetch user data --- assets/src/index.js | 38 +++- assets/src/utils.js | 7 + assets/styles/index.css | 8 + go.mod | 2 + go.sum | 4 + internal/errors.go | 3 + internal/jam.go | 404 +++++----------------------------------- internal/jam_tmp.go | 372 ++++++++++++++++++++++++++++++++++++ pages/index.html | 3 +- www/errors.go | 11 ++ www/routes.go | 65 ++++++- www/service.go | 7 + www/ws/conn.go | 3 + 13 files changed, 557 insertions(+), 370 deletions(-) create mode 100644 assets/src/utils.js create mode 100644 assets/styles/index.css create mode 100644 internal/jam_tmp.go create mode 100644 www/errors.go diff --git a/assets/src/index.js b/assets/src/index.js index c4136e5f..3e45629b 100644 --- a/assets/src/index.js +++ b/assets/src/index.js @@ -1,18 +1,33 @@ +import { websocketUrl } from "./utils.js"; + const app = document.getElementById("app"); app.innerHTML = `

This is a dynamic page

- + + +
    `; -const ws = new WebSocket(`ws://localhost:8888/jam`); +const ws = new WebSocket(websocketUrl("/jam")); ws.addEventListener("open", e => { + document.querySelector("button").disabled = false; + alert("web socket has opened"); }); -ws.addEventListener("message", e => { - alert(e.data); +ws.addEventListener("message", async e => { + const { type, id } = JSON.parse(e.data); + + switch (type.toLowerCase()) { + case "join": + userJoinedSession({ id }); + break; + case "leave": + userLeftSession({ id }); + break; + } }); ws.addEventListener("error", e => { @@ -23,4 +38,17 @@ ws.addEventListener("error", e => { document.querySelector("button").addEventListener("click", e => { ws.send(1); -}); \ No newline at end of file +}); + +function userJoinedSession({ id }) { + const li = document.createElement("li"); + li.textContent = `${id} has joined`; + li.id = id; + + document.querySelector(`[aria-label="users"]`).appendChild(li); +} + +function userLeftSession({ id }) { + const li = document.getElementById(id); + li.remove(); +} \ No newline at end of file diff --git a/assets/src/utils.js b/assets/src/utils.js new file mode 100644 index 00000000..ac8a0607 --- /dev/null +++ b/assets/src/utils.js @@ -0,0 +1,7 @@ +export function websocketUrl(path) { + var url = new URL(path, window.location.href); + + url.protocol = url.protocol.replace('http', 'ws'); + + return url.href; // => ws://www.example.com:9999/path/to/websocket +}; \ No newline at end of file diff --git a/assets/styles/index.css b/assets/styles/index.css new file mode 100644 index 00000000..00234317 --- /dev/null +++ b/assets/styles/index.css @@ -0,0 +1,8 @@ +@import url("https://cdn.jsdelivr.net/npm/modern-normalize/modern-normalize.min.css"); + +[aria-label="users"]::before { + content: attr(aria-label); + text-transform: capitalize; + font-size: large; + font-weight: bold; +} \ No newline at end of file diff --git a/go.mod b/go.mod index f7162007..213f297c 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/lithammer/shortuuid/v4 v4.0.0 github.com/magiconair/properties v1.8.6 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect @@ -28,6 +29,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.0 // indirect github.com/subosito/gotenv v1.3.0 // indirect + golang.org/x/exp v0.0.0-20220914170420-dc92f8653013 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index 5bf669b2..a59fe15b 100644 --- a/go.sum +++ b/go.sum @@ -144,6 +144,8 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= +github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -211,6 +213,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220914170420-dc92f8653013 h1:ZjglnWxEUdPyXl4o/j4T89SRCI+4X6NW6185PNLEOF4= +golang.org/x/exp v0.0.0-20220914170420-dc92f8653013/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/internal/errors.go b/internal/errors.go index 92d7e076..f1219a2e 100644 --- a/internal/errors.go +++ b/internal/errors.go @@ -1,11 +1,14 @@ package internal import ( + "errors" "log" "net/http" ) var ( + ErrTodo = errors.New("rmx: not yet implemented") + errNoCookie = errorResponse{status: http.StatusUnauthorized, message: "Cookie not found."} errSessionNotFound = errorResponse{status: http.StatusNotFound, message: "Session not found."} errSessionExists = errorResponse{status: http.StatusNotFound, message: "Session already exists."} diff --git a/internal/jam.go b/internal/jam.go index b06706dc..8df89ae8 100644 --- a/internal/jam.go +++ b/internal/jam.go @@ -1,372 +1,68 @@ package internal -import ( - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "sync" +type MessageTyp int - "github.com/go-chi/chi/v5" - "github.com/gobwas/ws" - "github.com/gobwas/ws/wsutil" - "github.com/google/uuid" -) - -// struct type to store info related to a websocket connection -type jamConn struct { - mu sync.Mutex - conn io.ReadWriter - - id string - username string -} - -// struct type to store info related to a Jam session -// also contains a map of current connections to the session -type jamSession struct { - mu sync.RWMutex - conns map[string]*jamConn - out chan []interface{} +const ( + Unknown = iota - id string - name string - tempo uint - owner string -} + Create + Delete -// struct type for the Jam service -// also contains current available sessions created by users -type JamService struct { - mu sync.RWMutex - sessions map[string]*jamSession -} + Join + Leave + Message -// request types -type ( - newJamReq struct { - Username string `json:"username"` - SessionName string `json:"session_name"` - Tempo uint `json:"tempo"` - } - - joinJamReq struct { - Username string `json:"username"` - SessionID string `json:"session_id"` - } - wsReq struct { - MessageType string `json:"messageType"` // Type of application message. Ex: JAM_SESSION_CONNECT, JAM_SESSION_CREATE - Payload struct{} `json:"payload"` // Payload of message, differs according to MessageType. Ex: JAM_SESSION_CREATE may contain Jam Session config (tempo, session name, etc). - } - - JamSlim struct { - Id string `json:"id"` - Name string `json:"name"` - } - listJamsResp struct { - Jams []JamSlim - } + NoteOn + NoteOff ) -func NewJamService() *JamService { - return &JamService{ - sessions: make(map[string]*jamSession), +func (t MessageTyp) String() string { + switch t { + case Create: + return "Create" + case Delete: + return "Delete" + case Join: + return "Join" + case Leave: + return "Leave" + case Message: + return "Message" + case NoteOn: + return "NoteOn" + case NoteOff: + return "NoteOff" + + default: + return "Unknown" } } -// new session handler -func (s *JamService) NewSession(w http.ResponseWriter, r *http.Request) { - // get values from the request - si := newJamReq{} - if err := parse(r, &si); err != nil { - log.Println(err) - w.WriteHeader(http.StatusBadRequest) - return - } - - // upgrade http connection to websocket - conn, _, _, err := ws.UpgradeHTTP(r, w) - if err != nil { - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // create a new connection between the owner and the server - // then add it to the session connections - c := jamConn{} - c.conn = conn - c.id = uuid.NewString() - if isEmptyString(si.Username) { - c.username = c.id - } else { - c.username = si.Username - } - - // create a new session and set the session owner - session := jamSession{ - conns: make(map[string]*jamConn), - out: make(chan []interface{}), - - id: uuid.NewString(), - name: si.SessionName, - tempo: si.Tempo, - owner: c.id, - } - // add session to sessions map - s.addSession(&session) - // check for errors - // err isn't nil if the username is already used - session.addConn(&c) - session.broadcast("Welcome to Rapidmidiex!") - - w.WriteHeader(http.StatusOK) -} - -// Connect establishes a WebSocket connection with the application. From there we can communicate with the client about which session to join. -func (s *JamService) Connect(w http.ResponseWriter, r *http.Request) { - // upgrade http connection to websocket - conn, _, _, err := ws.UpgradeHTTP(r, w) - if err != nil { - log.Println("connect", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - go func() { - defer conn.Close() - var ( - fr = wsutil.NewReader(conn, ws.StateServerSide) - fw = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText) - decoder = json.NewDecoder(fr) - encoder = json.NewEncoder(fw) - ) - for { - hdr, err := fr.NextFrame() - if err != nil { - log.Println(err) - } - if hdr.OpCode == ws.OpClose { - log.Println(io.EOF) - // Break out of for and close the connection - return - } - - // TODO: Hand off messages to some app level message broker - // Ex: JAM_SESSION_CONNECT, JAM_SESSION_CREATE - var req wsReq - if err := decoder.Decode(&req); err != nil { - log.Println(err) - } - - resp := listJamsResp{Jams: make([]JamSlim, 0)} - if err := encoder.Encode(&resp); err != nil { - log.Println(err) - } - if err = fw.Flush(); err != nil { - log.Println(err) - } - } - }() -} - -func (s *JamService) Join(w http.ResponseWriter, r *http.Request) { - // upgrade http connection to websocket - conn, _, _, err := ws.UpgradeHTTP(r, w) - if err != nil { - log.Println("connect", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - jamId := chi.URLParam(r, "jamId") - log.Println("jamId***", jamId) - - // Hardcoded new session for now - session := &jamSession{ - conns: make(map[string]*jamConn), - out: make(chan []interface{}), - - id: jamId, - name: "Jam On It!", - tempo: 85, - owner: uuid.NewString(), - } - - // Since the session is hardcoded, it probably already exists. - err = s.addSession(session) - // If the session does exist, we can ignore and keep rolling. - if err != nil && err != &errSessionExists { - handlerError(w, err) - return +func (t *MessageTyp) UnmarshalJSON(b []byte) error { + switch s := string(b[1 : len(b)-1]); s { + case "Create": + *t = Create + case "Delete": + *t = Delete + case "Join": + *t = Join + case "Leave": + *t = Leave + case "Message": + *t = Message + case "NoteOn": + *t = NoteOn + case "NoteOff": + *t = NoteOff + default: + *t = Unknown } - session, err = s.getSession(jamId) - if err != nil { - handlerError(w, err) - return - } - - go func() { - defer conn.Close() - var ( - fr = wsutil.NewReader(conn, ws.StateServerSide) - fw = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText) - decoder = json.NewDecoder(fr) - encoder = json.NewEncoder(fw) - ) - for { - hdr, err := fr.NextFrame() - if err != nil { - log.Println("NextFrame error", err) - } - if hdr.OpCode == ws.OpClose { - // Break out of for and close the connection - return - } - - // TODO: Hand off messages to some Jam level message handler - // Ex: NOTE_ON, NOTE_OFF, PLAYER_JOINED, PLAYER_MESSAGE - - // Should the message handler do the encoding/decoding since they will know which concrete type to decode into? - // TODO: Move all out to own handler - var req wsReq - if err := decoder.Decode(&req); err != nil { - log.Println(err) - } - - msg := fmt.Sprintf("Welcome to Jam %s!", session.id) - log.Println(msg) - // TODO: Create proper response types - type Resp struct { - MessageText string `json:"messageText"` - } - - resp := Resp{MessageText: msg} - if err := encoder.Encode(&resp); err != nil { - log.Println(err) - } - if err = fw.Flush(); err != nil { - log.Println(err) - } - } - }() -} - -// join session handler -func (s *JamService) JoinSession(w http.ResponseWriter, r *http.Request) { - // get values from the request - ji := joinJamReq{} - if err := parse(r, &ji); err != nil { - log.Println(err) - w.WriteHeader(http.StatusBadRequest) - return - } - - sID := chi.URLParam(r, "session_id") - - // err isn't nil if session doesn't exist - session, err := s.getSession(sID) - if err != nil { - handlerError(w, err) - return - } - - // upgrade http connection to websocket - conn, _, _, err := ws.UpgradeHTTP(r, w) - if err != nil { - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // create a new connection - c := jamConn{} - c.conn = conn - c.id = uuid.NewString() - if isEmptyString(ji.Username) { - c.username = c.id - } else { - c.username = ji.Username - } - session.addConn(&c) - session.broadcast("Welcome " + ji.Username + "!") - - w.WriteHeader(http.StatusOK) -} - -func (s *JamService) addSession(js *jamSession) error { - s.mu.Lock() - _, err := s.getSession(js.id) - if err != nil && err != &errSessionNotFound { - return &errSessionExists - } - s.sessions[js.id] = js - s.mu.Unlock() return nil } -func (s *JamService) getSession(sID string) (*jamSession, error) { - session, ok := s.sessions[sID] - if !ok { - return &jamSession{}, &errSessionNotFound - } - - return session, nil -} - -func (s *jamSession) addConn(jc *jamConn) { - s.mu.Lock() - s.conns[jc.id] = jc - s.mu.Unlock() -} - -func (c *jamConn) write(i interface{}) error { - w := wsutil.NewWriter(c.conn, ws.StateServerSide, ws.OpText) - encoder := json.NewEncoder(w) - - c.mu.Lock() - defer c.mu.Unlock() - - if err := encoder.Encode(i); err != nil { - return err - } - - return w.Flush() -} - -func (c *jamConn) writeRaw(b []byte) error { - c.mu.Lock() - defer c.mu.Unlock() - - _, err := c.conn.Write(b) - return err +func (t MessageTyp) MarshalJSON() ([]byte, error) { + return []byte(`"` + t.String() + `"`), nil } -// func (s *jamSession) writer(i interface{}) { -// for bts := range c.out { -// s.mu.RLock() -// cs := s.conns -// s.mu.RUnlock() -// -// for _, c := range cs { -// c := c // For closure. -// c.writeRaw(bts) -// } -// } -// } - -// iterates through session connections -// and send provided message to each of them -func (s *jamSession) broadcast(i interface{}) { - for _, c := range s.conns { - select { - case <-s.out: - c.write(i) - default: - delete(s.conns, c.username) - // c.close() - } - } -} +type ID string diff --git a/internal/jam_tmp.go b/internal/jam_tmp.go new file mode 100644 index 00000000..b06706dc --- /dev/null +++ b/internal/jam_tmp.go @@ -0,0 +1,372 @@ +package internal + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "sync" + + "github.com/go-chi/chi/v5" + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" + "github.com/google/uuid" +) + +// struct type to store info related to a websocket connection +type jamConn struct { + mu sync.Mutex + conn io.ReadWriter + + id string + username string +} + +// struct type to store info related to a Jam session +// also contains a map of current connections to the session +type jamSession struct { + mu sync.RWMutex + conns map[string]*jamConn + out chan []interface{} + + id string + name string + tempo uint + owner string +} + +// struct type for the Jam service +// also contains current available sessions created by users +type JamService struct { + mu sync.RWMutex + sessions map[string]*jamSession +} + +// request types +type ( + newJamReq struct { + Username string `json:"username"` + SessionName string `json:"session_name"` + Tempo uint `json:"tempo"` + } + + joinJamReq struct { + Username string `json:"username"` + SessionID string `json:"session_id"` + } + wsReq struct { + MessageType string `json:"messageType"` // Type of application message. Ex: JAM_SESSION_CONNECT, JAM_SESSION_CREATE + Payload struct{} `json:"payload"` // Payload of message, differs according to MessageType. Ex: JAM_SESSION_CREATE may contain Jam Session config (tempo, session name, etc). + } + + JamSlim struct { + Id string `json:"id"` + Name string `json:"name"` + } + listJamsResp struct { + Jams []JamSlim + } +) + +func NewJamService() *JamService { + return &JamService{ + sessions: make(map[string]*jamSession), + } +} + +// new session handler +func (s *JamService) NewSession(w http.ResponseWriter, r *http.Request) { + // get values from the request + si := newJamReq{} + if err := parse(r, &si); err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + return + } + + // upgrade http connection to websocket + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // create a new connection between the owner and the server + // then add it to the session connections + c := jamConn{} + c.conn = conn + c.id = uuid.NewString() + if isEmptyString(si.Username) { + c.username = c.id + } else { + c.username = si.Username + } + + // create a new session and set the session owner + session := jamSession{ + conns: make(map[string]*jamConn), + out: make(chan []interface{}), + + id: uuid.NewString(), + name: si.SessionName, + tempo: si.Tempo, + owner: c.id, + } + // add session to sessions map + s.addSession(&session) + // check for errors + // err isn't nil if the username is already used + session.addConn(&c) + session.broadcast("Welcome to Rapidmidiex!") + + w.WriteHeader(http.StatusOK) +} + +// Connect establishes a WebSocket connection with the application. From there we can communicate with the client about which session to join. +func (s *JamService) Connect(w http.ResponseWriter, r *http.Request) { + // upgrade http connection to websocket + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + log.Println("connect", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + go func() { + defer conn.Close() + var ( + fr = wsutil.NewReader(conn, ws.StateServerSide) + fw = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText) + decoder = json.NewDecoder(fr) + encoder = json.NewEncoder(fw) + ) + for { + hdr, err := fr.NextFrame() + if err != nil { + log.Println(err) + } + if hdr.OpCode == ws.OpClose { + log.Println(io.EOF) + // Break out of for and close the connection + return + } + + // TODO: Hand off messages to some app level message broker + // Ex: JAM_SESSION_CONNECT, JAM_SESSION_CREATE + var req wsReq + if err := decoder.Decode(&req); err != nil { + log.Println(err) + } + + resp := listJamsResp{Jams: make([]JamSlim, 0)} + if err := encoder.Encode(&resp); err != nil { + log.Println(err) + } + if err = fw.Flush(); err != nil { + log.Println(err) + } + } + }() +} + +func (s *JamService) Join(w http.ResponseWriter, r *http.Request) { + // upgrade http connection to websocket + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + log.Println("connect", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + jamId := chi.URLParam(r, "jamId") + log.Println("jamId***", jamId) + + // Hardcoded new session for now + session := &jamSession{ + conns: make(map[string]*jamConn), + out: make(chan []interface{}), + + id: jamId, + name: "Jam On It!", + tempo: 85, + owner: uuid.NewString(), + } + + // Since the session is hardcoded, it probably already exists. + err = s.addSession(session) + // If the session does exist, we can ignore and keep rolling. + if err != nil && err != &errSessionExists { + handlerError(w, err) + return + } + + session, err = s.getSession(jamId) + if err != nil { + handlerError(w, err) + return + } + + go func() { + defer conn.Close() + var ( + fr = wsutil.NewReader(conn, ws.StateServerSide) + fw = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText) + decoder = json.NewDecoder(fr) + encoder = json.NewEncoder(fw) + ) + for { + hdr, err := fr.NextFrame() + if err != nil { + log.Println("NextFrame error", err) + } + if hdr.OpCode == ws.OpClose { + // Break out of for and close the connection + return + } + + // TODO: Hand off messages to some Jam level message handler + // Ex: NOTE_ON, NOTE_OFF, PLAYER_JOINED, PLAYER_MESSAGE + + // Should the message handler do the encoding/decoding since they will know which concrete type to decode into? + // TODO: Move all out to own handler + var req wsReq + if err := decoder.Decode(&req); err != nil { + log.Println(err) + } + + msg := fmt.Sprintf("Welcome to Jam %s!", session.id) + log.Println(msg) + // TODO: Create proper response types + type Resp struct { + MessageText string `json:"messageText"` + } + + resp := Resp{MessageText: msg} + if err := encoder.Encode(&resp); err != nil { + log.Println(err) + } + if err = fw.Flush(); err != nil { + log.Println(err) + } + } + }() +} + +// join session handler +func (s *JamService) JoinSession(w http.ResponseWriter, r *http.Request) { + // get values from the request + ji := joinJamReq{} + if err := parse(r, &ji); err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + return + } + + sID := chi.URLParam(r, "session_id") + + // err isn't nil if session doesn't exist + session, err := s.getSession(sID) + if err != nil { + handlerError(w, err) + return + } + + // upgrade http connection to websocket + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // create a new connection + c := jamConn{} + c.conn = conn + c.id = uuid.NewString() + if isEmptyString(ji.Username) { + c.username = c.id + } else { + c.username = ji.Username + } + session.addConn(&c) + session.broadcast("Welcome " + ji.Username + "!") + + w.WriteHeader(http.StatusOK) +} + +func (s *JamService) addSession(js *jamSession) error { + s.mu.Lock() + _, err := s.getSession(js.id) + if err != nil && err != &errSessionNotFound { + return &errSessionExists + } + s.sessions[js.id] = js + s.mu.Unlock() + return nil +} + +func (s *JamService) getSession(sID string) (*jamSession, error) { + session, ok := s.sessions[sID] + if !ok { + return &jamSession{}, &errSessionNotFound + } + + return session, nil +} + +func (s *jamSession) addConn(jc *jamConn) { + s.mu.Lock() + s.conns[jc.id] = jc + s.mu.Unlock() +} + +func (c *jamConn) write(i interface{}) error { + w := wsutil.NewWriter(c.conn, ws.StateServerSide, ws.OpText) + encoder := json.NewEncoder(w) + + c.mu.Lock() + defer c.mu.Unlock() + + if err := encoder.Encode(i); err != nil { + return err + } + + return w.Flush() +} + +func (c *jamConn) writeRaw(b []byte) error { + c.mu.Lock() + defer c.mu.Unlock() + + _, err := c.conn.Write(b) + return err +} + +// func (s *jamSession) writer(i interface{}) { +// for bts := range c.out { +// s.mu.RLock() +// cs := s.conns +// s.mu.RUnlock() +// +// for _, c := range cs { +// c := c // For closure. +// c.writeRaw(bts) +// } +// } +// } + +// iterates through session connections +// and send provided message to each of them +func (s *jamSession) broadcast(i interface{}) { + for _, c := range s.conns { + select { + case <-s.out: + c.write(i) + default: + delete(s.conns, c.username) + // c.close() + } + } +} diff --git a/pages/index.html b/pages/index.html index 2ac5de0a..a3a314f4 100644 --- a/pages/index.html +++ b/pages/index.html @@ -6,7 +6,8 @@ Home - + + diff --git a/www/errors.go b/www/errors.go new file mode 100644 index 00000000..4f2a884d --- /dev/null +++ b/www/errors.go @@ -0,0 +1,11 @@ +package www + +import ( + "errors" +) + +var ( + ErrNoCookie = errors.New("www: cookie not found") + ErrSessionNotFound = errors.New("www: session not found") + ErrSessionExists = errors.New("www: session already exists") +) diff --git a/www/routes.go b/www/routes.go index 8a7c65b9..ad47a6f2 100644 --- a/www/routes.go +++ b/www/routes.go @@ -3,17 +3,25 @@ package www import ( "net/http" + "github.com/go-chi/chi/v5/middleware" + t "github.com/hyphengolang/prelude/template" + + rmx "github.com/rog-golang-buddies/rapidmidiex/internal" "github.com/rog-golang-buddies/rapidmidiex/www/ws" ) func (s Service) routes() { + s.r.Use(middleware.Logger) + // http s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) s.r.Handle("/", s.handleIndexHTML("pages/index.html")) + // api + // ws - s.r.HandleFunc("/jam", chain(s.handleJamSessionWS, s.upgradeHTTP)) + s.r.HandleFunc("/jam", chain(s.handleJamSession(), s.upgradeHTTP)) } func (s Service) handleIndexHTML(path string) http.HandlerFunc { @@ -27,20 +35,57 @@ func (s Service) handleIndexHTML(path string) http.HandlerFunc { } } -func (s Service) handleJamSessionWS(w http.ResponseWriter, r *http.Request) { - c := r.Context().Value(upgradeKey).(*ws.Conn) - defer c.Close() +func (s Service) handleJamSession() http.HandlerFunc { + type join struct { + MessageTyp rmx.MessageTyp `json:"type"` + ID rmx.ID `json:"id"` + SessionID rmx.ID `json:"session_id"` + // Users []rmx.ID `json:"users"` + } + + type leave struct { + MessageTyp rmx.MessageTyp `json:"type"` + ID rmx.ID `json:"id"` + SessionID rmx.ID `json:"session_id"` + } + + return func(w http.ResponseWriter, r *http.Request) { + c := r.Context().Value(upgradeKey).(*ws.Conn) + defer func() { + c.SendMessage(leave{ + MessageTyp: rmx.Leave, + ID: s.SafeUUID(c.ID), + SessionID: s.SafeUUID(c.Pool().ID), + }) + + c.Close() + }() + + err := c.SendMessage(join{ + MessageTyp: rmx.Join, + ID: s.SafeUUID(c.ID), + SessionID: s.SafeUUID(c.Pool().ID), + // grabbing user info should be handled by a proper RESTful endpoint + // Can be used alongside JS fetch API + // Users: n/a, + }) - for { - var n int - if err := c.ReadJSON(&n); err != nil { + if err != nil { s.l.Println(err) return } - if err := c.SendMessage(n + 10); err != nil { - s.l.Println(err) - return + for { + var n int + if err := c.ReadJSON(&n); err != nil { + s.l.Println(err) + return + } + + if err := c.SendMessage(n + 10); err != nil { + s.l.Println(err) + return + } } } } diff --git a/www/service.go b/www/service.go index d0dd5ca4..f0d57570 100644 --- a/www/service.go +++ b/www/service.go @@ -5,9 +5,12 @@ import ( "net/http" "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/lithammer/shortuuid/v4" h "github.com/hyphengolang/prelude/http" + rmx "github.com/rog-golang-buddies/rapidmidiex/internal" "github.com/rog-golang-buddies/rapidmidiex/www/ws" ) @@ -33,3 +36,7 @@ func (s Service) respond(w http.ResponseWriter, r *http.Request, data interface{ func (s Service) fileServer(prefix string, dirname string) http.Handler { return h.FileServer(prefix, dirname) } + +func (s Service) SafeUUID(uid uuid.UUID) rmx.ID { + return rmx.ID(shortuuid.DefaultEncoder.Encode(uid)) +} diff --git a/www/ws/conn.go b/www/ws/conn.go index 27a63960..f2938a49 100644 --- a/www/ws/conn.go +++ b/www/ws/conn.go @@ -12,8 +12,11 @@ type Conn struct { p *Pool } +func (c Conn) Pool() *Pool { return c.p } + func (c Conn) Close() error { c.p.Delete(c.ID) + return c.rwc.Close() } From b743f6be9d8a43273c7f30d8b76fe22b60681546 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 15 Sep 2022 04:00:52 +0100 Subject: [PATCH 017/246] multiple sessions with dynamic number of users per session --- assets/src/index.js | 55 +++------------ assets/src/play.js | 66 ++++++++++++++++++ assets/src/utils.js | 11 ++- go.mod | 2 +- internal/errors.go | 22 +----- internal/{jam.go => rmx.go} | 1 + internal/tmp/errors.go | 27 ++++++++ internal/{ => tmp}/jam_test.go | 0 internal/{ => tmp}/jam_tmp.go | 0 internal/{ => tmp}/server.go | 0 internal/{ => tmp}/utils.go | 0 pages/404.html | 15 ++++ pages/play.html | 16 +++++ www/middleware.go | 33 +++++++-- www/routes.go | 122 +++++++++++++++++++++++++++------ www/service.go | 16 +++-- www/ws/client.go | 62 +++++++++++++++++ www/ws/conn.go | 2 +- www/ws/errors.go | 6 ++ www/ws/pool.go | 15 +++- 20 files changed, 368 insertions(+), 103 deletions(-) create mode 100644 assets/src/play.js rename internal/{jam.go => rmx.go} (94%) create mode 100644 internal/tmp/errors.go rename internal/{ => tmp}/jam_test.go (100%) rename internal/{ => tmp}/jam_tmp.go (100%) rename internal/{ => tmp}/server.go (100%) rename internal/{ => tmp}/utils.go (100%) create mode 100644 pages/404.html create mode 100644 pages/play.html create mode 100644 www/ws/client.go create mode 100644 www/ws/errors.go diff --git a/assets/src/index.js b/assets/src/index.js index 3e45629b..6d22d7cc 100644 --- a/assets/src/index.js +++ b/assets/src/index.js @@ -3,52 +3,17 @@ import { websocketUrl } from "./utils.js"; const app = document.getElementById("app"); app.innerHTML = ` -

    This is a dynamic page

    - - -
      +

      Home

      +

      To create a new session tap the button below

      + `; -const ws = new WebSocket(websocketUrl("/jam")); - -ws.addEventListener("open", e => { - document.querySelector("button").disabled = false; - - alert("web socket has opened"); -}); - -ws.addEventListener("message", async e => { - const { type, id } = JSON.parse(e.data); - - switch (type.toLowerCase()) { - case "join": - userJoinedSession({ id }); - break; - case "leave": - userLeftSession({ id }); - break; +document.querySelector("button").addEventListener("click", async e => { + try { + const r = await fetch("/api/jam/create"); + const { sessionId } = await r.json(); + console.log(sessionId); + } catch (e) { + console.error(e.message); } }); - -ws.addEventListener("error", e => { - console.error(e); - - document.querySelector("button").disabled = true; -}); - -document.querySelector("button").addEventListener("click", e => { - ws.send(1); -}); - -function userJoinedSession({ id }) { - const li = document.createElement("li"); - li.textContent = `${id} has joined`; - li.id = id; - - document.querySelector(`[aria-label="users"]`).appendChild(li); -} - -function userLeftSession({ id }) { - const li = document.getElementById(id); - li.remove(); -} \ No newline at end of file diff --git a/assets/src/play.js b/assets/src/play.js new file mode 100644 index 00000000..4569e4c7 --- /dev/null +++ b/assets/src/play.js @@ -0,0 +1,66 @@ +import { websocketUrl, sessionId } from "./utils.js"; + +const app = document.getElementById("app"); + +app.innerHTML = ` +

      Welcome to the JAM Session

      + + +
        +`; + +const ws = new WebSocket(websocketUrl(`/jam/${sessionId()}`)); + +ws.addEventListener("open", e => { + document.querySelector("button").disabled = false; + + alert("web socket has opened"); +}); + +ws.addEventListener("message", async e => { + const { type, id } = JSON.parse(e.data); + + switch (type.toLowerCase()) { + case "join": + userJoinedSession({ id }); + break; + case "leave": + userLeftSession({ id }); + break; + } +}); + +ws.addEventListener("error", e => { + console.error(e); + + document.querySelector("button").disabled = true; +}); + +document.querySelector("button").addEventListener("click", e => { + ws.send(1); +}); + +async function userJoinedSession({ id }) { + const r = await fetch(`/api/jam/${sessionId()}`); + const { userIds } = await r.json(); + + //? proxy Array that updates the list in the DOM + + const items = []; + for (const id of userIds) { + const li = document.createElement("li"); + li.textContent = `${id} has joined`; + li.id = id; + items.push(li); + } + + document + .querySelector(`[aria-label="users"]`) + .replaceChildren(...items); + // document.querySelector(`[aria-label="users"]`).appendChild(li); +} + +function userLeftSession({ id }) { + const li = document.getElementById(id); + li.remove(); +} \ No newline at end of file diff --git a/assets/src/utils.js b/assets/src/utils.js index ac8a0607..a8f6c7c2 100644 --- a/assets/src/utils.js +++ b/assets/src/utils.js @@ -4,4 +4,13 @@ export function websocketUrl(path) { url.protocol = url.protocol.replace('http', 'ws'); return url.href; // => ws://www.example.com:9999/path/to/websocket -}; \ No newline at end of file +}; + +/** + * + * @returns {string} sessionId + */ +export const sessionId = () => { + return window.location.pathname.split("/").at(-1); +} + diff --git a/go.mod b/go.mod index 213f297c..9447cbe6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/rog-golang-buddies/rapidmidiex -go 1.18 +go 1.19 require ( github.com/go-chi/chi v1.5.4 diff --git a/internal/errors.go b/internal/errors.go index f1219a2e..1267c216 100644 --- a/internal/errors.go +++ b/internal/errors.go @@ -2,26 +2,6 @@ package internal import ( "errors" - "log" - "net/http" ) -var ( - ErrTodo = errors.New("rmx: not yet implemented") - - errNoCookie = errorResponse{status: http.StatusUnauthorized, message: "Cookie not found."} - errSessionNotFound = errorResponse{status: http.StatusNotFound, message: "Session not found."} - errSessionExists = errorResponse{status: http.StatusNotFound, message: "Session already exists."} -) - -func handlerError(w http.ResponseWriter, err error) { - if err != nil { - if httpError, ok := err.(*errorResponse); ok { - http.Error(w, httpError.message, httpError.status) - return - } - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} +var ErrTodo = errors.New("rmx: not yet implemented") diff --git a/internal/jam.go b/internal/rmx.go similarity index 94% rename from internal/jam.go rename to internal/rmx.go index 8df89ae8..0c6daea8 100644 --- a/internal/jam.go +++ b/internal/rmx.go @@ -65,4 +65,5 @@ func (t MessageTyp) MarshalJSON() ([]byte, error) { return []byte(`"` + t.String() + `"`), nil } +// ID with ability to encode to a URL friendly version type ID string diff --git a/internal/tmp/errors.go b/internal/tmp/errors.go new file mode 100644 index 00000000..f1219a2e --- /dev/null +++ b/internal/tmp/errors.go @@ -0,0 +1,27 @@ +package internal + +import ( + "errors" + "log" + "net/http" +) + +var ( + ErrTodo = errors.New("rmx: not yet implemented") + + errNoCookie = errorResponse{status: http.StatusUnauthorized, message: "Cookie not found."} + errSessionNotFound = errorResponse{status: http.StatusNotFound, message: "Session not found."} + errSessionExists = errorResponse{status: http.StatusNotFound, message: "Session already exists."} +) + +func handlerError(w http.ResponseWriter, err error) { + if err != nil { + if httpError, ok := err.(*errorResponse); ok { + http.Error(w, httpError.message, httpError.status) + return + } + log.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } +} diff --git a/internal/jam_test.go b/internal/tmp/jam_test.go similarity index 100% rename from internal/jam_test.go rename to internal/tmp/jam_test.go diff --git a/internal/jam_tmp.go b/internal/tmp/jam_tmp.go similarity index 100% rename from internal/jam_tmp.go rename to internal/tmp/jam_tmp.go diff --git a/internal/server.go b/internal/tmp/server.go similarity index 100% rename from internal/server.go rename to internal/tmp/server.go diff --git a/internal/utils.go b/internal/tmp/utils.go similarity index 100% rename from internal/utils.go rename to internal/tmp/utils.go diff --git a/pages/404.html b/pages/404.html new file mode 100644 index 00000000..4ca98714 --- /dev/null +++ b/pages/404.html @@ -0,0 +1,15 @@ + + + + + + + + 404, Oh No! + + + +

        404 Error

        + + + \ No newline at end of file diff --git a/pages/play.html b/pages/play.html new file mode 100644 index 00000000..c34d1275 --- /dev/null +++ b/pages/play.html @@ -0,0 +1,16 @@ + + + + + + + + Jam Session + + + + +
        + + + \ No newline at end of file diff --git a/www/middleware.go b/www/middleware.go index 0eb46cef..efbb7032 100644 --- a/www/middleware.go +++ b/www/middleware.go @@ -2,12 +2,34 @@ package www import ( "context" - "fmt" "net/http" "github.com/gorilla/websocket" + + ws "github.com/rog-golang-buddies/rapidmidiex/www/ws" ) +func (s Service) sessionPool(f http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + uid, err := s.parseUUID(w, r, "id") + if err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + p, err := s.c.Get(uid) + if err != nil { + s.l.Println(err) + return + } + + s.l.Println("This session matches the ID", p.ID) + + r = r.WithContext(context.WithValue(r.Context(), roomKey, p)) + f(w, r) + } +} + func (s Service) upgradeHTTP(f http.HandlerFunc) http.HandlerFunc { u := &websocket.Upgrader{ ReadBufferSize: 1024, @@ -16,13 +38,14 @@ func (s Service) upgradeHTTP(f http.HandlerFunc) http.HandlerFunc { } return func(w http.ResponseWriter, r *http.Request) { - if s.p.Size() == s.p.MaxConn { - err := fmt.Errorf("pool: maximum number of connections reached") - s.respond(w, r, err, http.StatusUnauthorized) + p := r.Context().Value(roomKey).(*ws.Pool) + + if p.Size() == p.MaxConn { + s.respond(w, r, ws.ErrMaxConn, http.StatusUnauthorized) return } - c, err := s.p.NewConn(w, r, u) + c, err := p.NewConn(w, r, u) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) return diff --git a/www/routes.go b/www/routes.go index ad47a6f2..efc6d889 100644 --- a/www/routes.go +++ b/www/routes.go @@ -4,27 +4,33 @@ import ( "net/http" "github.com/go-chi/chi/v5/middleware" + "github.com/google/uuid" + suid "github.com/lithammer/shortuuid/v4" t "github.com/hyphengolang/prelude/template" rmx "github.com/rog-golang-buddies/rapidmidiex/internal" - "github.com/rog-golang-buddies/rapidmidiex/www/ws" + ws "github.com/rog-golang-buddies/rapidmidiex/www/ws" ) func (s Service) routes() { + // middleware s.r.Use(middleware.Logger) // http s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) - s.r.Handle("/", s.handleIndexHTML("pages/index.html")) + s.r.Get("/", s.indexHTML("pages/index.html")) + s.r.Get("/play/{id}", s.jamSessionHTML("pages/play.html")) // api + s.r.Get("/api/jam/create", s.createSession()) + s.r.Get("/api/jam/{id}", s.getSessionData()) // ws - s.r.HandleFunc("/jam", chain(s.handleJamSession(), s.upgradeHTTP)) + s.r.HandleFunc("/jam/{id}", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) } -func (s Service) handleIndexHTML(path string) http.HandlerFunc { +func (s Service) indexHTML(path string) http.HandlerFunc { render, err := t.Render(path) if err != nil { panic(err) @@ -35,39 +41,53 @@ func (s Service) handleIndexHTML(path string) http.HandlerFunc { } } -func (s Service) handleJamSession() http.HandlerFunc { - type join struct { - MessageTyp rmx.MessageTyp `json:"type"` - ID rmx.ID `json:"id"` - SessionID rmx.ID `json:"session_id"` - // Users []rmx.ID `json:"users"` +func (s Service) jamSessionHTML(path string) http.HandlerFunc { + render, err := t.Render(path) + if err != nil { + panic(err) + } + + // I should be rendering a 404 page if there is an error + // in this layer, but for an MVC this will do + return func(w http.ResponseWriter, r *http.Request) { + uid, err := s.parseUUID(w, r, "id") + if err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + if _, err = s.c.Get(uid); err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + render(w, r, nil) } +} - type leave struct { +func (s Service) handleJamSession() http.HandlerFunc { + type response struct { MessageTyp rmx.MessageTyp `json:"type"` ID rmx.ID `json:"id"` - SessionID rmx.ID `json:"session_id"` + SessionID rmx.ID `json:"sessionId"` } return func(w http.ResponseWriter, r *http.Request) { c := r.Context().Value(upgradeKey).(*ws.Conn) defer func() { - c.SendMessage(leave{ + c.SendMessage(response{ MessageTyp: rmx.Leave, - ID: s.SafeUUID(c.ID), - SessionID: s.SafeUUID(c.Pool().ID), + ID: s.safeUUID(c.ID), + SessionID: s.safeUUID(c.Pool().ID), }) c.Close() }() - err := c.SendMessage(join{ + err := c.SendMessage(response{ MessageTyp: rmx.Join, - ID: s.SafeUUID(c.ID), - SessionID: s.SafeUUID(c.Pool().ID), - // grabbing user info should be handled by a proper RESTful endpoint - // Can be used alongside JS fetch API - // Users: n/a, + ID: s.safeUUID(c.ID), + SessionID: s.safeUUID(c.Pool().ID), }) if err != nil { @@ -89,3 +109,63 @@ func (s Service) handleJamSession() http.HandlerFunc { } } } + +func (s Service) createSession() http.HandlerFunc { + type response struct { + ID rmx.ID `json:"sessionId"` + } + + return func(w http.ResponseWriter, r *http.Request) { + uid, err := s.c.NewPool() + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + v := response{ + ID: rmx.ID(suid.DefaultEncoder.Encode(uid)), + } + + s.respond(w, r, v, http.StatusOK) + } +} + +func (s Service) getSessionData() http.HandlerFunc { + type response struct { + ID rmx.ID `json:"sessionId"` + Users []rmx.ID `json:"userIds"` + } + + return func(w http.ResponseWriter, r *http.Request) { + uid, err := s.parseUUID(w, r, "id") + if err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + p, err := s.c.Get(uid) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + v := &response{ + ID: rmx.ID(suid.DefaultEncoder.Encode(p.ID)), + Users: FMap(p.Keys(), func(uid uuid.UUID) rmx.ID { + return rmx.ID(suid.DefaultEncoder.Encode(uid)) + }), + } + + s.respond(w, r, v, http.StatusOK) + } +} + +func FMap[T any, U any](vs []T, f func(T) U) []U { + out := make([]U, len(vs)) + + for i, v := range vs { + out[i] = f(v) + } + + return out +} diff --git a/www/service.go b/www/service.go index f0d57570..9b2100d5 100644 --- a/www/service.go +++ b/www/service.go @@ -6,25 +6,25 @@ import ( "github.com/go-chi/chi/v5" "github.com/google/uuid" - "github.com/lithammer/shortuuid/v4" + suid "github.com/lithammer/shortuuid/v4" h "github.com/hyphengolang/prelude/http" rmx "github.com/rog-golang-buddies/rapidmidiex/internal" - "github.com/rog-golang-buddies/rapidmidiex/www/ws" + ws "github.com/rog-golang-buddies/rapidmidiex/www/ws" ) type Service struct { r chi.Router l *log.Logger - p *ws.Pool + c *ws.Client } func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } func NewService(r chi.Router) *Service { - s := &Service{r, log.Default(), ws.DefaultPool()} + s := &Service{r, log.Default(), ws.DefaultClient} s.routes() return s } @@ -37,6 +37,10 @@ func (s Service) fileServer(prefix string, dirname string) http.Handler { return h.FileServer(prefix, dirname) } -func (s Service) SafeUUID(uid uuid.UUID) rmx.ID { - return rmx.ID(shortuuid.DefaultEncoder.Encode(uid)) +func (s Service) safeUUID(uid uuid.UUID) rmx.ID { + return rmx.ID(suid.DefaultEncoder.Encode(uid)) +} + +func (S Service) parseUUID(w http.ResponseWriter, r *http.Request, key string) (uuid.UUID, error) { + return suid.DefaultEncoder.Decode(chi.URLParam(r, key)) } diff --git a/www/ws/client.go b/www/ws/client.go new file mode 100644 index 00000000..1bdf02bb --- /dev/null +++ b/www/ws/client.go @@ -0,0 +1,62 @@ +package websocket + +import ( + "sync" + + "github.com/google/uuid" + + rmx "github.com/rog-golang-buddies/rapidmidiex/internal" +) + +type Client struct { + mu sync.Mutex + + ps map[uuid.UUID]*Pool +} + +var DefaultClient = &Client{ + ps: make(map[uuid.UUID]*Pool), +} + +func NewClient() *Client { + c := &Client{ + ps: make(map[uuid.UUID]*Pool), + } + + return c +} + +func (c *Client) Size() int { return len(c.ps) } + +func (c *Client) Close() error { + return rmx.ErrTodo +} + +func (c *Client) NewPool() (uuid.UUID, error) { + c.mu.Lock() + defer c.mu.Unlock() + + p := DefaultPool() + + c.ps[p.ID] = p + + return p.ID, nil +} + +func (c *Client) Get(uid uuid.UUID) (*Pool, error) { + c.mu.Lock() + defer c.mu.Unlock() + + for id, p := range c.ps { + if id == uid { + return p, nil + } + } + + return nil, ErrNoPool +} + +func (c *Client) Has(uid uuid.UUID) bool { + _, err := c.Get(uid) + return err == nil +} diff --git a/www/ws/conn.go b/www/ws/conn.go index f2938a49..1637d4ba 100644 --- a/www/ws/conn.go +++ b/www/ws/conn.go @@ -1,4 +1,4 @@ -package ws +package websocket import ( "github.com/google/uuid" diff --git a/www/ws/errors.go b/www/ws/errors.go new file mode 100644 index 00000000..3dc87b24 --- /dev/null +++ b/www/ws/errors.go @@ -0,0 +1,6 @@ +package websocket + +import "errors" + +var ErrNoPool = errors.New("ws: pool does not exist") +var ErrMaxConn = errors.New("ws: maximum number of connections reached") diff --git a/www/ws/pool.go b/www/ws/pool.go index 1b977166..b5acce84 100644 --- a/www/ws/pool.go +++ b/www/ws/pool.go @@ -1,4 +1,4 @@ -package ws +package websocket import ( "net/http" @@ -6,6 +6,10 @@ import ( "github.com/google/uuid" "github.com/gorilla/websocket" + + // https://stackoverflow.com/questions/21362950/getting-a-slice-of-keys-from-a-map + + "golang.org/x/exp/maps" ) type Pool struct { @@ -21,7 +25,7 @@ type Pool struct { func DefaultPool() *Pool { p := &Pool{ ID: uuid.New(), - MaxConn: 2, + MaxConn: 4, cs: make(map[uuid.UUID]*Conn), msgs: make(chan any), } @@ -46,6 +50,13 @@ func (p *Pool) Size() int { return len(p.cs) } +func (p *Pool) Keys() []uuid.UUID { + p.mu.Lock() + defer p.mu.Unlock() + + return maps.Keys(p.cs) +} + func (p *Pool) NewConn(w http.ResponseWriter, r *http.Request, u *websocket.Upgrader) (*Conn, error) { p.mu.Lock() defer p.mu.Unlock() From 5bafa8c0fee5e114f2adab1b937a385c478e7428 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 15 Sep 2022 09:33:10 +0100 Subject: [PATCH 018/246] added comments and utililty functions --- assets/src/index.js | 23 ++++++++++++++---- assets/src/play.js | 8 +++---- cmd/app.go | 54 ++++++++++++++++++++++++++++++++++++++++++ cmd/main.go | 17 +++++++------ internal/tmp/server.go | 2 +- internal/utils.go | 11 +++++++++ www/middleware.go | 2 -- www/routes.go | 30 ++++++++++------------- www/ws/pool.go | 5 ++++ 9 files changed, 116 insertions(+), 36 deletions(-) create mode 100644 cmd/app.go create mode 100644 internal/utils.go diff --git a/assets/src/index.js b/assets/src/index.js index 6d22d7cc..91167ee8 100644 --- a/assets/src/index.js +++ b/assets/src/index.js @@ -1,19 +1,34 @@ -import { websocketUrl } from "./utils.js"; - const app = document.getElementById("app"); app.innerHTML = ` -

        Home

        +

        Welcome, Home

        To create a new session tap the button below

        + +
        `; document.querySelector("button").addEventListener("click", async e => { try { const r = await fetch("/api/jam/create"); const { sessionId } = await r.json(); - console.log(sessionId); + + session.id = sessionId; } catch (e) { console.error(e.message); } }); + +const session = new Proxy({ id: "" }, { + set(obj, prop, value) { + switch (prop) { + case "id": + obj[prop] = value; + // !should not be hard-coded but for MVC it's acceptable + document.getElementById("session").textContent = window.location.href + "play/" + value; + return true; + + default: return false; + } + } +}); \ No newline at end of file diff --git a/assets/src/play.js b/assets/src/play.js index 4569e4c7..1f92344a 100644 --- a/assets/src/play.js +++ b/assets/src/play.js @@ -42,12 +42,13 @@ document.querySelector("button").addEventListener("click", e => { async function userJoinedSession({ id }) { const r = await fetch(`/api/jam/${sessionId()}`); - const { userIds } = await r.json(); + const { users } = await r.json(); - //? proxy Array that updates the list in the DOM + console.log(users); + // ^proxy Array that updates the list in the DOM const items = []; - for (const id of userIds) { + for (const id of users) { const li = document.createElement("li"); li.textContent = `${id} has joined`; li.id = id; @@ -57,7 +58,6 @@ async function userJoinedSession({ id }) { document .querySelector(`[aria-label="users"]`) .replaceChildren(...items); - // document.querySelector(`[aria-label="users"]`).appendChild(li); } function userLeftSession({ id }) { diff --git a/cmd/app.go b/cmd/app.go new file mode 100644 index 00000000..b0d076bf --- /dev/null +++ b/cmd/app.go @@ -0,0 +1,54 @@ +package main + +import ( + "context" + "log" + "net/http" + "time" + + "github.com/go-chi/chi/v5" + "github.com/rog-golang-buddies/rapidmidiex/www" + "github.com/rs/cors" +) + +type App struct { + srv *http.Server + l *log.Logger +} + +func NewApp(addr string, h http.Handler, read, write, idle time.Duration) *App { + c := cors.Options{ + AllowedOrigins: []string{"http://localhost" + addr}, + AllowCredentials: true, + AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodPatch}, + AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + } + + a := &App{ + srv: &http.Server{ + Addr: addr, + Handler: cors.New(c).Handler(h), + ReadTimeout: read, + WriteTimeout: write, + IdleTimeout: idle, + }, + l: log.Default(), + } + + return a +} + +func DefaultApp() *App { + mux := chi.NewMux() + h := www.NewService(mux) + + return NewApp(":8888", h, 10*time.Second, 10*time.Second, 120*time.Second) +} + +func (a App) Shutdown() error { + return a.srv.Shutdown(context.Background()) +} + +func (a *App) ListenAndServe() error { + return a.srv.ListenAndServe() +} diff --git a/cmd/main.go b/cmd/main.go index 4fcd1829..733aa7d4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,14 +2,17 @@ package main import ( "log" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/rog-golang-buddies/rapidmidiex/www" ) func main() { - s := www.NewService(chi.NewMux()) - log.Println("http://localhost:8888") - log.Fatalln(http.ListenAndServe(":8888", s)) + if err := run(); err != nil { + log.Fatalln(err) + } +} + +func run() error { + app := DefaultApp() + app.l.Printf("Running... http://localhost%s/", app.srv.Addr) + + return app.ListenAndServe() } diff --git a/internal/tmp/server.go b/internal/tmp/server.go index b7366025..348109c0 100644 --- a/internal/tmp/server.go +++ b/internal/tmp/server.go @@ -24,7 +24,7 @@ type Server struct { func (s *Server) ServeHTTP() { // "*" shouldn't be used as AllowedOrigins c := cors.Options{ - AllowedOrigins: []string{"http://localhost:3000", "http://127.0.0.1:3000"}, + AllowedOrigins: []string{"http://localhost:8888", "http://127.0.0.1:8888"}, AllowCredentials: true, AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodPatch}, AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, diff --git a/internal/utils.go b/internal/utils.go new file mode 100644 index 00000000..101b187f --- /dev/null +++ b/internal/utils.go @@ -0,0 +1,11 @@ +package internal + +func FMap[T any, U any](vs []T, f func(T) U) (us []U) { + us = make([]U, len(vs)) + + for i, v := range vs { + us[i] = f(v) + } + + return +} diff --git a/www/middleware.go b/www/middleware.go index efbb7032..8047e012 100644 --- a/www/middleware.go +++ b/www/middleware.go @@ -23,8 +23,6 @@ func (s Service) sessionPool(f http.HandlerFunc) http.HandlerFunc { return } - s.l.Println("This session matches the ID", p.ID) - r = r.WithContext(context.WithValue(r.Context(), roomKey, p)) f(w, r) } diff --git a/www/routes.go b/www/routes.go index efc6d889..4e6c7717 100644 --- a/www/routes.go +++ b/www/routes.go @@ -47,8 +47,8 @@ func (s Service) jamSessionHTML(path string) http.HandlerFunc { panic(err) } - // I should be rendering a 404 page if there is an error - // in this layer, but for an MVC this will do + // !I should be rendering a 404 page if there is an error + // !in this layer, but for an MVC this will do return func(w http.ResponseWriter, r *http.Request) { uid, err := s.parseUUID(w, r, "id") if err != nil { @@ -95,6 +95,9 @@ func (s Service) handleJamSession() http.HandlerFunc { return } + // ?could the API be adjusted such that + // ?this for-loop only needs to read and + // ?never touch the code for writing for { var n int if err := c.ReadJSON(&n); err != nil { @@ -112,7 +115,7 @@ func (s Service) handleJamSession() http.HandlerFunc { func (s Service) createSession() http.HandlerFunc { type response struct { - ID rmx.ID `json:"sessionId"` + SessionID rmx.ID `json:"sessionId"` } return func(w http.ResponseWriter, r *http.Request) { @@ -123,7 +126,7 @@ func (s Service) createSession() http.HandlerFunc { } v := response{ - ID: rmx.ID(suid.DefaultEncoder.Encode(uid)), + SessionID: rmx.ID(suid.DefaultEncoder.Encode(uid)), } s.respond(w, r, v, http.StatusOK) @@ -132,8 +135,8 @@ func (s Service) createSession() http.HandlerFunc { func (s Service) getSessionData() http.HandlerFunc { type response struct { - ID rmx.ID `json:"sessionId"` - Users []rmx.ID `json:"userIds"` + SessionID rmx.ID `json:"sessionId"` + Users []rmx.ID `json:"users"` } return func(w http.ResponseWriter, r *http.Request) { @@ -143,6 +146,7 @@ func (s Service) getSessionData() http.HandlerFunc { return } + // !rename method as `Get` is undescriptive p, err := s.c.Get(uid) if err != nil { s.respond(w, r, err, http.StatusNotFound) @@ -150,8 +154,8 @@ func (s Service) getSessionData() http.HandlerFunc { } v := &response{ - ID: rmx.ID(suid.DefaultEncoder.Encode(p.ID)), - Users: FMap(p.Keys(), func(uid uuid.UUID) rmx.ID { + SessionID: rmx.ID(suid.DefaultEncoder.Encode(p.ID)), + Users: rmx.FMap(p.Keys(), func(uid uuid.UUID) rmx.ID { return rmx.ID(suid.DefaultEncoder.Encode(uid)) }), } @@ -159,13 +163,3 @@ func (s Service) getSessionData() http.HandlerFunc { s.respond(w, r, v, http.StatusOK) } } - -func FMap[T any, U any](vs []T, f func(T) U) []U { - out := make([]U, len(vs)) - - for i, v := range vs { - out[i] = f(v) - } - - return out -} diff --git a/www/ws/pool.go b/www/ws/pool.go index b5acce84..53ef812b 100644 --- a/www/ws/pool.go +++ b/www/ws/pool.go @@ -38,6 +38,11 @@ func DefaultPool() *Pool { c.WriteJSON(msg) } } + + // ?why does this pattern not work + // for _, c := range p.cs { + // c.WriteJSON(<-p.msgs) + // } }() return p From 0c6f48efed25dee9d9032b46868ca9806eeeccda Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 15 Sep 2022 09:43:37 +0100 Subject: [PATCH 019/246] tidy & rename --- assets/src/play.js | 10 +++++----- www/ws/pool.go | 4 +--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/assets/src/play.js b/assets/src/play.js index 1f92344a..a71830a0 100644 --- a/assets/src/play.js +++ b/assets/src/play.js @@ -22,10 +22,10 @@ ws.addEventListener("message", async e => { switch (type.toLowerCase()) { case "join": - userJoinedSession({ id }); + newUserJoined({ id }); break; case "leave": - userLeftSession({ id }); + userHasLeft({ id }); break; } }); @@ -40,11 +40,11 @@ document.querySelector("button").addEventListener("click", e => { ws.send(1); }); -async function userJoinedSession({ id }) { +async function newUserJoined({ id }) { const r = await fetch(`/api/jam/${sessionId()}`); const { users } = await r.json(); - console.log(users); + // console.log(users); // ^proxy Array that updates the list in the DOM const items = []; @@ -60,7 +60,7 @@ async function userJoinedSession({ id }) { .replaceChildren(...items); } -function userLeftSession({ id }) { +function userHasLeft({ id }) { const li = document.getElementById(id); li.remove(); } \ No newline at end of file diff --git a/www/ws/pool.go b/www/ws/pool.go index 53ef812b..9637b8a9 100644 --- a/www/ws/pool.go +++ b/www/ws/pool.go @@ -40,9 +40,7 @@ func DefaultPool() *Pool { } // ?why does this pattern not work - // for _, c := range p.cs { - // c.WriteJSON(<-p.msgs) - // } + // for _, c := range p.cs { c.WriteJSON(<-p.msgs) } }() return p From 33d9189c6727fc6c19585ec922eef0e4bc8ed7de Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 15 Sep 2022 09:45:09 +0100 Subject: [PATCH 020/246] remove unnecessaryfunction param --- assets/src/play.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/src/play.js b/assets/src/play.js index a71830a0..e08bc8c5 100644 --- a/assets/src/play.js +++ b/assets/src/play.js @@ -22,7 +22,7 @@ ws.addEventListener("message", async e => { switch (type.toLowerCase()) { case "join": - newUserJoined({ id }); + newUserJoined(); break; case "leave": userHasLeft({ id }); @@ -40,7 +40,7 @@ document.querySelector("button").addEventListener("click", e => { ws.send(1); }); -async function newUserJoined({ id }) { +async function newUserJoined() { const r = await fetch(`/api/jam/${sessionId()}`); const { users } = await r.json(); From 7b8ee5eb9796deed1bc1d7959a882b4643498307 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 15 Sep 2022 10:33:09 +0100 Subject: [PATCH 021/246] clean up play view --- assets/src/play.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/assets/src/play.js b/assets/src/play.js index e08bc8c5..87115207 100644 --- a/assets/src/play.js +++ b/assets/src/play.js @@ -25,7 +25,7 @@ ws.addEventListener("message", async e => { newUserJoined(); break; case "leave": - userHasLeft({ id }); + document.getElementById(id).remove(); break; } }); @@ -58,9 +58,4 @@ async function newUserJoined() { document .querySelector(`[aria-label="users"]`) .replaceChildren(...items); -} - -function userHasLeft({ id }) { - const li = document.getElementById(id); - li.remove(); } \ No newline at end of file From fad8c66a1547caf5dee81d3942461b14f9fd1e18 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 15 Sep 2022 11:09:20 +0100 Subject: [PATCH 022/246] adding comments for harvey --- assets/src/play.js | 2 - assets/styles/play.css | 0 internal/tmp/errors.go | 4 +- internal/tmp/jam_test.go | 3 +- internal/tmp/jam_tmp.go | 120 +++++++++++++++++++++------------------ internal/tmp/server.go | 4 +- internal/tmp/utils.go | 6 +- www/ws/client.go | 2 +- www/ws/pool.go | 2 +- 9 files changed, 79 insertions(+), 64 deletions(-) create mode 100644 assets/styles/play.css diff --git a/assets/src/play.js b/assets/src/play.js index 87115207..064e7509 100644 --- a/assets/src/play.js +++ b/assets/src/play.js @@ -44,8 +44,6 @@ async function newUserJoined() { const r = await fetch(`/api/jam/${sessionId()}`); const { users } = await r.json(); - // console.log(users); - // ^proxy Array that updates the list in the DOM const items = []; for (const id of users) { diff --git a/assets/styles/play.css b/assets/styles/play.css new file mode 100644 index 00000000..e69de29b diff --git a/internal/tmp/errors.go b/internal/tmp/errors.go index f1219a2e..39717df4 100644 --- a/internal/tmp/errors.go +++ b/internal/tmp/errors.go @@ -1,4 +1,6 @@ -package internal +// * most of this isn't required as I have already got a function that handles http responses +// * file:jam_tmp.go is dependant therefore cannot delete this straight away +package tmp import ( "errors" diff --git a/internal/tmp/jam_test.go b/internal/tmp/jam_test.go index 36ab509d..e51cf62a 100644 --- a/internal/tmp/jam_test.go +++ b/internal/tmp/jam_test.go @@ -1,4 +1,5 @@ -package internal +// * Will be adding the testing back once we can adapt it for the `gorilla/websocket` package +package tmp import ( "context" diff --git a/internal/tmp/jam_tmp.go b/internal/tmp/jam_tmp.go index b06706dc..6659763e 100644 --- a/internal/tmp/jam_tmp.go +++ b/internal/tmp/jam_tmp.go @@ -1,4 +1,6 @@ -package internal +// * In the process of taking useful code from here and moving to the `www` package + +package tmp import ( "encoding/json" @@ -23,6 +25,28 @@ type jamConn struct { username string } +func (c *jamConn) write(i interface{}) error { + w := wsutil.NewWriter(c.conn, ws.StateServerSide, ws.OpText) + encoder := json.NewEncoder(w) + + c.mu.Lock() + defer c.mu.Unlock() + + if err := encoder.Encode(i); err != nil { + return err + } + + return w.Flush() +} + +func (c *jamConn) writeRaw(b []byte) error { + c.mu.Lock() + defer c.mu.Unlock() + + _, err := c.conn.Write(b) + return err +} + // struct type to store info related to a Jam session // also contains a map of current connections to the session type jamSession struct { @@ -36,6 +60,43 @@ type jamSession struct { owner string } +func (s *jamSession) addConn(jc *jamConn) { + s.mu.Lock() + s.conns[jc.id] = jc + s.mu.Unlock() +} + +// func (s *jamSession) writer(i interface{}) { +// for bts := range c.out { +// s.mu.RLock() +// cs := s.conns +// s.mu.RUnlock() +// +// for _, c := range cs { +// c := c // For closure. +// c.writeRaw(bts) +// } +// } +// } + +// * I have adapted this slightly inside the `www/ws` pacakge +// iterates through session connections +// and send provided message to each of them +func (s *jamSession) broadcast(i interface{}) { + for _, c := range s.conns { + select { + case <-s.out: + c.write(i) + default: + delete(s.conns, c.username) + // c.close() + } + } +} + +// * Imo, a service should have a `ServeHTTP` method attatched if it is going to be talking +// * directly to the web, I have adapted this inside `www` pacakge +// * I have also decoupled the RESTful logic with sessions into a seperate data structure // struct type for the Jam service // also contains current available sessions created by users type JamService struct { @@ -43,6 +104,8 @@ type JamService struct { sessions map[string]*jamSession } +// * Better to define some of these inside the handlers themselves +// * An example of such pattern can be found inside the `www` pacakge // request types type ( newJamReq struct { @@ -315,58 +378,3 @@ func (s *JamService) getSession(sID string) (*jamSession, error) { return session, nil } - -func (s *jamSession) addConn(jc *jamConn) { - s.mu.Lock() - s.conns[jc.id] = jc - s.mu.Unlock() -} - -func (c *jamConn) write(i interface{}) error { - w := wsutil.NewWriter(c.conn, ws.StateServerSide, ws.OpText) - encoder := json.NewEncoder(w) - - c.mu.Lock() - defer c.mu.Unlock() - - if err := encoder.Encode(i); err != nil { - return err - } - - return w.Flush() -} - -func (c *jamConn) writeRaw(b []byte) error { - c.mu.Lock() - defer c.mu.Unlock() - - _, err := c.conn.Write(b) - return err -} - -// func (s *jamSession) writer(i interface{}) { -// for bts := range c.out { -// s.mu.RLock() -// cs := s.conns -// s.mu.RUnlock() -// -// for _, c := range cs { -// c := c // For closure. -// c.writeRaw(bts) -// } -// } -// } - -// iterates through session connections -// and send provided message to each of them -func (s *jamSession) broadcast(i interface{}) { - for _, c := range s.conns { - select { - case <-s.out: - c.write(i) - default: - delete(s.conns, c.username) - // c.close() - } - } -} diff --git a/internal/tmp/server.go b/internal/tmp/server.go index 348109c0..296fdfb7 100644 --- a/internal/tmp/server.go +++ b/internal/tmp/server.go @@ -1,4 +1,6 @@ -package internal +// * Need to move a lot of this logic back into the `cmd` package +// * I have started this process already +package tmp import ( "context" diff --git a/internal/tmp/utils.go b/internal/tmp/utils.go index a326a6f0..ee1116fa 100644 --- a/internal/tmp/utils.go +++ b/internal/tmp/utils.go @@ -1,4 +1,8 @@ -package internal +// * This file is not being used but saving as I have questions regarding the `isEmptyString` function +// * Parse not required as is the case with the errorResponse as those should really live in the `www` package +// * As there are function that depend on these utlities, I have refrained from deleting + +package tmp import ( "encoding/json" diff --git a/www/ws/client.go b/www/ws/client.go index 1bdf02bb..7f171b50 100644 --- a/www/ws/client.go +++ b/www/ws/client.go @@ -9,7 +9,7 @@ import ( ) type Client struct { - mu sync.Mutex + mu sync.RWMutex ps map[uuid.UUID]*Pool } diff --git a/www/ws/pool.go b/www/ws/pool.go index 9637b8a9..9bc7fa1d 100644 --- a/www/ws/pool.go +++ b/www/ws/pool.go @@ -13,7 +13,7 @@ import ( ) type Pool struct { - mu sync.Mutex + mu sync.RWMutex ID uuid.UUID MaxConn int From cd6d5645d84890e20b40221876b86410e91dcd60 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Thu, 15 Sep 2022 10:43:46 -0400 Subject: [PATCH 023/246] Fix VSCode config; Use PORT env var --- .vscode/launch.json | 6 ++++-- cmd/app.go | 11 ++++++++++- go.mod | 2 ++ go.sum | 4 ++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 73c99c11..5e3c488f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,11 +5,13 @@ "version": "0.2.0", "configurations": [ { - "name": "Run Server", + "name": "Launch Package", "type": "go", "request": "launch", "mode": "auto", - "program": "${workspaceFolder}/cmd/server/main.go" + "program": "${workspaceFolder}/cmd", + "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env" } ] } diff --git a/cmd/app.go b/cmd/app.go index b0d076bf..550017ec 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -4,6 +4,7 @@ import ( "context" "log" "net/http" + "os" "time" "github.com/go-chi/chi/v5" @@ -41,8 +42,9 @@ func NewApp(addr string, h http.Handler, read, write, idle time.Duration) *App { func DefaultApp() *App { mux := chi.NewMux() h := www.NewService(mux) + port := getEnv("PORT", "8888") - return NewApp(":8888", h, 10*time.Second, 10*time.Second, 120*time.Second) + return NewApp(":"+port, h, 10*time.Second, 10*time.Second, 120*time.Second) } func (a App) Shutdown() error { @@ -52,3 +54,10 @@ func (a App) Shutdown() error { func (a *App) ListenAndServe() error { return a.srv.ListenAndServe() } + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} diff --git a/go.mod b/go.mod index 9447cbe6..26860196 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/go-chi/chi/v5 v5.0.7 github.com/gobwas/ws v1.1.0 github.com/google/uuid v1.3.0 + github.com/gorilla/websocket v1.5.0 + github.com/hyphengolang/prelude v0.0.7 github.com/rs/cors v1.8.2 github.com/spf13/viper v1.12.0 golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde diff --git a/go.sum b/go.sum index a59fe15b..4ba254b9 100644 --- a/go.sum +++ b/go.sum @@ -128,10 +128,14 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hyphengolang/prelude v0.0.7 h1:AgLkbASkg/3ioIjr0JinhReTiaeppIaX2sa7vrFwaoc= +github.com/hyphengolang/prelude v0.0.7/go.mod h1:obKnO4SQhPfu3gqA9g29nbDRQLNTtmJy02i7KYZGdfM= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= From 042ff6b1998fa2c62a741b0bb0e792711917e1c3 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 16 Sep 2022 10:34:00 +0100 Subject: [PATCH 024/246] readed graceful shutdown to `cmd` package --- cmd/app.go | 63 --- cmd/main.go | 11 +- cmd/run.go | 77 +++ internal/list/list.go | 94 ++++ internal/list/list.h | 1118 +++++++++++++++++++++++++++++++++++++++ internal/tmp/jam_tmp.go | 1 - internal/tmp/utils.go | 1 - www/routes.go | 4 +- 8 files changed, 1292 insertions(+), 77 deletions(-) delete mode 100644 cmd/app.go create mode 100644 cmd/run.go create mode 100644 internal/list/list.go create mode 100644 internal/list/list.h diff --git a/cmd/app.go b/cmd/app.go deleted file mode 100644 index 550017ec..00000000 --- a/cmd/app.go +++ /dev/null @@ -1,63 +0,0 @@ -package main - -import ( - "context" - "log" - "net/http" - "os" - "time" - - "github.com/go-chi/chi/v5" - "github.com/rog-golang-buddies/rapidmidiex/www" - "github.com/rs/cors" -) - -type App struct { - srv *http.Server - l *log.Logger -} - -func NewApp(addr string, h http.Handler, read, write, idle time.Duration) *App { - c := cors.Options{ - AllowedOrigins: []string{"http://localhost" + addr}, - AllowCredentials: true, - AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodPatch}, - AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, - } - - a := &App{ - srv: &http.Server{ - Addr: addr, - Handler: cors.New(c).Handler(h), - ReadTimeout: read, - WriteTimeout: write, - IdleTimeout: idle, - }, - l: log.Default(), - } - - return a -} - -func DefaultApp() *App { - mux := chi.NewMux() - h := www.NewService(mux) - port := getEnv("PORT", "8888") - - return NewApp(":"+port, h, 10*time.Second, 10*time.Second, 120*time.Second) -} - -func (a App) Shutdown() error { - return a.srv.Shutdown(context.Background()) -} - -func (a *App) ListenAndServe() error { - return a.srv.ListenAndServe() -} - -func getEnv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - return fallback -} diff --git a/cmd/main.go b/cmd/main.go index 733aa7d4..2f468f2a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,18 +1,9 @@ package main -import ( - "log" -) +import "log" func main() { if err := run(); err != nil { log.Fatalln(err) } } - -func run() error { - app := DefaultApp() - app.l.Printf("Running... http://localhost%s/", app.srv.Addr) - - return app.ListenAndServe() -} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 00000000..4fec1cbd --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,77 @@ +package main + +import ( + "context" + "log" + "net" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/go-chi/chi/v5" + "github.com/rs/cors" + "golang.org/x/sync/errgroup" + + "github.com/rog-golang-buddies/rapidmidiex/www" +) + +func run() error { + port := getEnv("PORT", "8888") + + c := cors.Options{ + AllowedOrigins: []string{"http://localhost:" + port}, + AllowCredentials: true, + AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodPatch}, + AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + } + + mux := chi.NewMux() + service := www.NewService(mux) + + h := cors.New(c).Handler(service) + + serverCtx, serverStop := signal.NotifyContext( + context.Background(), + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + defer serverStop() + + server := http.Server{ + Addr: ":" + port, + Handler: h, + ReadTimeout: 10 * time.Second, // max time to read request from the client + WriteTimeout: 10 * time.Second, // max time to write response to the client + IdleTimeout: 120 * time.Second, // max time for connections using TCP Keep-Alive + BaseContext: func(_ net.Listener) context.Context { + return serverCtx + }, + } + + g, gCtx := errgroup.WithContext(serverCtx) + g.Go(func() error { + // Run the server + return server.ListenAndServe() + }) + g.Go(func() error { + <-gCtx.Done() + return server.Shutdown(context.Background()) + }) + + if err := g.Wait(); err != nil { + log.Printf("exit reason: %s \n", err) + } + + return nil +} + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} diff --git a/internal/list/list.go b/internal/list/list.go new file mode 100644 index 00000000..a584821c --- /dev/null +++ b/internal/list/list.go @@ -0,0 +1,94 @@ +package list + +type foo struct { + value int + next, prev *foo +} + +type bar struct { + value int + ptr list +} + +type list struct { + next, prev *list +} + +func addToTail(node, head *list) { + listAdd(node, head.prev, head) +} + +func listAdd(node, prev, next *list) { + if !listAddValid(node, prev, next) { + return + } + + next.prev = node + node.next = next + node.prev = prev + writeOnce(prev.next, node) +} + +func listAddValid(node, prev, next *list) bool { return true } + +func writeOnce(x, val any) { + x = val +} + +func listForEach(pos, head *list, f func()) { + for pos = head.next; !listIsHead(pos, head); pos = pos.next { + f() + } + + // for (pos = (head)->next; !list_is_head(pos, (head)); pos = pos->next) +} + +func listIsHead(list, head *list) bool { return list == head } + +/* +static inline void list_add_tail(struct list_head *new, struct list_head *head) +{ +__list_add(new, head->prev, head); +} + +static inline void __list_add(struct list_head *new, +struct list_head *prev, +struct list_head *next) +{ +if (!__list_add_valid(new, prev, next)) +return; +next->prev = new; +new->next = next; +new->prev = prev; +WRITE_ONCE(prev->next, new); +} + +extern bool __list_add_valid(struct list_head *new,struct list_head *prev, struct list_head *next); +extern bool __list_del_entry_valid(struct list_head *entry); +#else +static inline bool __list_add_valid(struct list_head *new, +struct list_head *prev, +struct list_head *next) +{ +return true; +} + +static inline int list_is_head(const struct list_head *list, const struct list_head *head) +{ +return list == head; +} + +#define list_for_each_entry(pos, head, member) \ + for (pos = list_first_entry(head, typeof(*pos), member); \ + !list_entry_is_head(pos, head, member); \ + pos = list_next_entry(pos, member)) + +#define list_entry_is_head(pos, head, member) \ + (&pos->member == (head)) + + #define list_next_entry(pos, member) \ + list_entry((pos)->member.next, typeof(*(pos)), member) + + #define list_entry(ptr, type, member) \ + container_of(ptr, type, member) +*/ diff --git a/internal/list/list.h b/internal/list/list.h new file mode 100644 index 00000000..006368a0 --- /dev/null +++ b/internal/list/list.h @@ -0,0 +1,1118 @@ +Skip to content +Search or jump to… +Pull requests +Issues +Marketplace +Explore + +@adoublef +torvalds +/ +linux +Public +Code +Pull requests +313 +Actions +Projects +Security +Insights +linux/include/linux/list.h +@torvalds +torvalds Merge tag 'mm-nonmm-stable-2022-05-26' of git://git.kernel.org/pub/sc… +… +Latest commit 6f66404 on 27 May + History + 59 contributors +@paulmckrcu@torvalds@rddunlap@rpjday@utrace@dhowells@djbw@andy-shev@kees@jpirko@cmetcalf-tilera@masahir0y +1074 lines (974 sloc) 31.5 KB + +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef _LINUX_LIST_H +#define _LINUX_LIST_H + +#include +#include +#include +#include +#include + +#include + +/* + * Circular doubly linked list implementation. + * + * Some of the internal functions ("__xxx") are useful when + * manipulating whole lists rather than single entries, as + * sometimes we already know the next/prev entries and we can + * generate better code by using them directly rather than + * using the generic single-entry routines. + */ + +#define LIST_HEAD_INIT(name) { &(name), &(name) } + +#define LIST_HEAD(name) \ + struct list_head name = LIST_HEAD_INIT(name) + +/** + * INIT_LIST_HEAD - Initialize a list_head structure + * @list: list_head structure to be initialized. + * + * Initializes the list_head to point to itself. If it is a list header, + * the result is an empty list. + */ +static inline void INIT_LIST_HEAD(struct list_head *list) +{ + WRITE_ONCE(list->next, list); + WRITE_ONCE(list->prev, list); +} + +#ifdef CONFIG_DEBUG_LIST +extern bool __list_add_valid(struct list_head *new, + struct list_head *prev, + struct list_head *next); +extern bool __list_del_entry_valid(struct list_head *entry); +#else +static inline bool __list_add_valid(struct list_head *new, + struct list_head *prev, + struct list_head *next) +{ + return true; +} +static inline bool __list_del_entry_valid(struct list_head *entry) +{ + return true; +} +#endif + +/* + * Insert a new entry between two known consecutive entries. + * + * This is only for internal list manipulation where we know + * the prev/next entries already! + */ +static inline void __list_add(struct list_head *new, + struct list_head *prev, + struct list_head *next) +{ + if (!__list_add_valid(new, prev, next)) + return; + + next->prev = new; + new->next = next; + new->prev = prev; + WRITE_ONCE(prev->next, new); +} + +/** + * list_add - add a new entry + * @new: new entry to be added + * @head: list head to add it after + * + * Insert a new entry after the specified head. + * This is good for implementing stacks. + */ +static inline void list_add(struct list_head *new, struct list_head *head) +{ + __list_add(new, head, head->next); +} + + +/** + * list_add_tail - add a new entry + * @new: new entry to be added + * @head: list head to add it before + * + * Insert a new entry before the specified head. + * This is useful for implementing queues. + */ +static inline void list_add_tail(struct list_head *new, struct list_head *head) +{ + __list_add(new, head->prev, head); +} + +/* + * Delete a list entry by making the prev/next entries + * point to each other. + * + * This is only for internal list manipulation where we know + * the prev/next entries already! + */ +static inline void __list_del(struct list_head * prev, struct list_head * next) +{ + next->prev = prev; + WRITE_ONCE(prev->next, next); +} + +/* + * Delete a list entry and clear the 'prev' pointer. + * + * This is a special-purpose list clearing method used in the networking code + * for lists allocated as per-cpu, where we don't want to incur the extra + * WRITE_ONCE() overhead of a regular list_del_init(). The code that uses this + * needs to check the node 'prev' pointer instead of calling list_empty(). + */ +static inline void __list_del_clearprev(struct list_head *entry) +{ + __list_del(entry->prev, entry->next); + entry->prev = NULL; +} + +static inline void __list_del_entry(struct list_head *entry) +{ + if (!__list_del_entry_valid(entry)) + return; + + __list_del(entry->prev, entry->next); +} + +/** + * list_del - deletes entry from list. + * @entry: the element to delete from the list. + * Note: list_empty() on entry does not return true after this, the entry is + * in an undefined state. + */ +static inline void list_del(struct list_head *entry) +{ + __list_del_entry(entry); + entry->next = LIST_POISON1; + entry->prev = LIST_POISON2; +} + +/** + * list_replace - replace old entry by new one + * @old : the element to be replaced + * @new : the new element to insert + * + * If @old was empty, it will be overwritten. + */ +static inline void list_replace(struct list_head *old, + struct list_head *new) +{ + new->next = old->next; + new->next->prev = new; + new->prev = old->prev; + new->prev->next = new; +} + +/** + * list_replace_init - replace old entry by new one and initialize the old one + * @old : the element to be replaced + * @new : the new element to insert + * + * If @old was empty, it will be overwritten. + */ +static inline void list_replace_init(struct list_head *old, + struct list_head *new) +{ + list_replace(old, new); + INIT_LIST_HEAD(old); +} + +/** + * list_swap - replace entry1 with entry2 and re-add entry1 at entry2's position + * @entry1: the location to place entry2 + * @entry2: the location to place entry1 + */ +static inline void list_swap(struct list_head *entry1, + struct list_head *entry2) +{ + struct list_head *pos = entry2->prev; + + list_del(entry2); + list_replace(entry1, entry2); + if (pos == entry1) + pos = entry2; + list_add(entry1, pos); +} + +/** + * list_del_init - deletes entry from list and reinitialize it. + * @entry: the element to delete from the list. + */ +static inline void list_del_init(struct list_head *entry) +{ + __list_del_entry(entry); + INIT_LIST_HEAD(entry); +} + +/** + * list_move - delete from one list and add as another's head + * @list: the entry to move + * @head: the head that will precede our entry + */ +static inline void list_move(struct list_head *list, struct list_head *head) +{ + __list_del_entry(list); + list_add(list, head); +} + +/** + * list_move_tail - delete from one list and add as another's tail + * @list: the entry to move + * @head: the head that will follow our entry + */ +static inline void list_move_tail(struct list_head *list, + struct list_head *head) +{ + __list_del_entry(list); + list_add_tail(list, head); +} + +/** + * list_bulk_move_tail - move a subsection of a list to its tail + * @head: the head that will follow our entry + * @first: first entry to move + * @last: last entry to move, can be the same as first + * + * Move all entries between @first and including @last before @head. + * All three entries must belong to the same linked list. + */ +static inline void list_bulk_move_tail(struct list_head *head, + struct list_head *first, + struct list_head *last) +{ + first->prev->next = last->next; + last->next->prev = first->prev; + + head->prev->next = first; + first->prev = head->prev; + + last->next = head; + head->prev = last; +} + +/** + * list_is_first -- tests whether @list is the first entry in list @head + * @list: the entry to test + * @head: the head of the list + */ +static inline int list_is_first(const struct list_head *list, const struct list_head *head) +{ + return list->prev == head; +} + +/** + * list_is_last - tests whether @list is the last entry in list @head + * @list: the entry to test + * @head: the head of the list + */ +static inline int list_is_last(const struct list_head *list, const struct list_head *head) +{ + return list->next == head; +} + +/** + * list_is_head - tests whether @list is the list @head + * @list: the entry to test + * @head: the head of the list + */ +static inline int list_is_head(const struct list_head *list, const struct list_head *head) +{ + return list == head; +} + +/** + * list_empty - tests whether a list is empty + * @head: the list to test. + */ +static inline int list_empty(const struct list_head *head) +{ + return READ_ONCE(head->next) == head; +} + +/** + * list_del_init_careful - deletes entry from list and reinitialize it. + * @entry: the element to delete from the list. + * + * This is the same as list_del_init(), except designed to be used + * together with list_empty_careful() in a way to guarantee ordering + * of other memory operations. + * + * Any memory operations done before a list_del_init_careful() are + * guaranteed to be visible after a list_empty_careful() test. + */ +static inline void list_del_init_careful(struct list_head *entry) +{ + __list_del_entry(entry); + WRITE_ONCE(entry->prev, entry); + smp_store_release(&entry->next, entry); +} + +/** + * list_empty_careful - tests whether a list is empty and not being modified + * @head: the list to test + * + * Description: + * tests whether a list is empty _and_ checks that no other CPU might be + * in the process of modifying either member (next or prev) + * + * NOTE: using list_empty_careful() without synchronization + * can only be safe if the only activity that can happen + * to the list entry is list_del_init(). Eg. it cannot be used + * if another CPU could re-list_add() it. + */ +static inline int list_empty_careful(const struct list_head *head) +{ + struct list_head *next = smp_load_acquire(&head->next); + return list_is_head(next, head) && (next == READ_ONCE(head->prev)); +} + +/** + * list_rotate_left - rotate the list to the left + * @head: the head of the list + */ +static inline void list_rotate_left(struct list_head *head) +{ + struct list_head *first; + + if (!list_empty(head)) { + first = head->next; + list_move_tail(first, head); + } +} + +/** + * list_rotate_to_front() - Rotate list to specific item. + * @list: The desired new front of the list. + * @head: The head of the list. + * + * Rotates list so that @list becomes the new front of the list. + */ +static inline void list_rotate_to_front(struct list_head *list, + struct list_head *head) +{ + /* + * Deletes the list head from the list denoted by @head and + * places it as the tail of @list, this effectively rotates the + * list so that @list is at the front. + */ + list_move_tail(head, list); +} + +/** + * list_is_singular - tests whether a list has just one entry. + * @head: the list to test. + */ +static inline int list_is_singular(const struct list_head *head) +{ + return !list_empty(head) && (head->next == head->prev); +} + +static inline void __list_cut_position(struct list_head *list, + struct list_head *head, struct list_head *entry) +{ + struct list_head *new_first = entry->next; + list->next = head->next; + list->next->prev = list; + list->prev = entry; + entry->next = list; + head->next = new_first; + new_first->prev = head; +} + +/** + * list_cut_position - cut a list into two + * @list: a new list to add all removed entries + * @head: a list with entries + * @entry: an entry within head, could be the head itself + * and if so we won't cut the list + * + * This helper moves the initial part of @head, up to and + * including @entry, from @head to @list. You should + * pass on @entry an element you know is on @head. @list + * should be an empty list or a list you do not care about + * losing its data. + * + */ +static inline void list_cut_position(struct list_head *list, + struct list_head *head, struct list_head *entry) +{ + if (list_empty(head)) + return; + if (list_is_singular(head) && !list_is_head(entry, head) && (entry != head->next)) + return; + if (list_is_head(entry, head)) + INIT_LIST_HEAD(list); + else + __list_cut_position(list, head, entry); +} + +/** + * list_cut_before - cut a list into two, before given entry + * @list: a new list to add all removed entries + * @head: a list with entries + * @entry: an entry within head, could be the head itself + * + * This helper moves the initial part of @head, up to but + * excluding @entry, from @head to @list. You should pass + * in @entry an element you know is on @head. @list should + * be an empty list or a list you do not care about losing + * its data. + * If @entry == @head, all entries on @head are moved to + * @list. + */ +static inline void list_cut_before(struct list_head *list, + struct list_head *head, + struct list_head *entry) +{ + if (head->next == entry) { + INIT_LIST_HEAD(list); + return; + } + list->next = head->next; + list->next->prev = list; + list->prev = entry->prev; + list->prev->next = list; + head->next = entry; + entry->prev = head; +} + +static inline void __list_splice(const struct list_head *list, + struct list_head *prev, + struct list_head *next) +{ + struct list_head *first = list->next; + struct list_head *last = list->prev; + + first->prev = prev; + prev->next = first; + + last->next = next; + next->prev = last; +} + +/** + * list_splice - join two lists, this is designed for stacks + * @list: the new list to add. + * @head: the place to add it in the first list. + */ +static inline void list_splice(const struct list_head *list, + struct list_head *head) +{ + if (!list_empty(list)) + __list_splice(list, head, head->next); +} + +/** + * list_splice_tail - join two lists, each list being a queue + * @list: the new list to add. + * @head: the place to add it in the first list. + */ +static inline void list_splice_tail(struct list_head *list, + struct list_head *head) +{ + if (!list_empty(list)) + __list_splice(list, head->prev, head); +} + +/** + * list_splice_init - join two lists and reinitialise the emptied list. + * @list: the new list to add. + * @head: the place to add it in the first list. + * + * The list at @list is reinitialised + */ +static inline void list_splice_init(struct list_head *list, + struct list_head *head) +{ + if (!list_empty(list)) { + __list_splice(list, head, head->next); + INIT_LIST_HEAD(list); + } +} + +/** + * list_splice_tail_init - join two lists and reinitialise the emptied list + * @list: the new list to add. + * @head: the place to add it in the first list. + * + * Each of the lists is a queue. + * The list at @list is reinitialised + */ +static inline void list_splice_tail_init(struct list_head *list, + struct list_head *head) +{ + if (!list_empty(list)) { + __list_splice(list, head->prev, head); + INIT_LIST_HEAD(list); + } +} + +/** + * list_entry - get the struct for this entry + * @ptr: the &struct list_head pointer. + * @type: the type of the struct this is embedded in. + * @member: the name of the list_head within the struct. + */ +#define list_entry(ptr, type, member) \ + container_of(ptr, type, member) + +/** + * list_first_entry - get the first element from a list + * @ptr: the list head to take the element from. + * @type: the type of the struct this is embedded in. + * @member: the name of the list_head within the struct. + * + * Note, that list is expected to be not empty. + */ +#define list_first_entry(ptr, type, member) \ + list_entry((ptr)->next, type, member) + +/** + * list_last_entry - get the last element from a list + * @ptr: the list head to take the element from. + * @type: the type of the struct this is embedded in. + * @member: the name of the list_head within the struct. + * + * Note, that list is expected to be not empty. + */ +#define list_last_entry(ptr, type, member) \ + list_entry((ptr)->prev, type, member) + +/** + * list_first_entry_or_null - get the first element from a list + * @ptr: the list head to take the element from. + * @type: the type of the struct this is embedded in. + * @member: the name of the list_head within the struct. + * + * Note that if the list is empty, it returns NULL. + */ +#define list_first_entry_or_null(ptr, type, member) ({ \ + struct list_head *head__ = (ptr); \ + struct list_head *pos__ = READ_ONCE(head__->next); \ + pos__ != head__ ? list_entry(pos__, type, member) : NULL; \ +}) + +/** + * list_next_entry - get the next element in list + * @pos: the type * to cursor + * @member: the name of the list_head within the struct. + */ +#define list_next_entry(pos, member) \ + list_entry((pos)->member.next, typeof(*(pos)), member) + +/** + * list_next_entry_circular - get the next element in list + * @pos: the type * to cursor. + * @head: the list head to take the element from. + * @member: the name of the list_head within the struct. + * + * Wraparound if pos is the last element (return the first element). + * Note, that list is expected to be not empty. + */ +#define list_next_entry_circular(pos, head, member) \ + (list_is_last(&(pos)->member, head) ? \ + list_first_entry(head, typeof(*(pos)), member) : list_next_entry(pos, member)) + +/** + * list_prev_entry - get the prev element in list + * @pos: the type * to cursor + * @member: the name of the list_head within the struct. + */ +#define list_prev_entry(pos, member) \ + list_entry((pos)->member.prev, typeof(*(pos)), member) + +/** + * list_prev_entry_circular - get the prev element in list + * @pos: the type * to cursor. + * @head: the list head to take the element from. + * @member: the name of the list_head within the struct. + * + * Wraparound if pos is the first element (return the last element). + * Note, that list is expected to be not empty. + */ +#define list_prev_entry_circular(pos, head, member) \ + (list_is_first(&(pos)->member, head) ? \ + list_last_entry(head, typeof(*(pos)), member) : list_prev_entry(pos, member)) + +/** + * list_for_each - iterate over a list + * @pos: the &struct list_head to use as a loop cursor. + * @head: the head for your list. + */ +#define list_for_each(pos, head) \ + for (pos = (head)->next; !list_is_head(pos, (head)); pos = pos->next) + +/** + * list_for_each_rcu - Iterate over a list in an RCU-safe fashion + * @pos: the &struct list_head to use as a loop cursor. + * @head: the head for your list. + */ +#define list_for_each_rcu(pos, head) \ + for (pos = rcu_dereference((head)->next); \ + !list_is_head(pos, (head)); \ + pos = rcu_dereference(pos->next)) + +/** + * list_for_each_continue - continue iteration over a list + * @pos: the &struct list_head to use as a loop cursor. + * @head: the head for your list. + * + * Continue to iterate over a list, continuing after the current position. + */ +#define list_for_each_continue(pos, head) \ + for (pos = pos->next; !list_is_head(pos, (head)); pos = pos->next) + +/** + * list_for_each_prev - iterate over a list backwards + * @pos: the &struct list_head to use as a loop cursor. + * @head: the head for your list. + */ +#define list_for_each_prev(pos, head) \ + for (pos = (head)->prev; !list_is_head(pos, (head)); pos = pos->prev) + +/** + * list_for_each_safe - iterate over a list safe against removal of list entry + * @pos: the &struct list_head to use as a loop cursor. + * @n: another &struct list_head to use as temporary storage + * @head: the head for your list. + */ +#define list_for_each_safe(pos, n, head) \ + for (pos = (head)->next, n = pos->next; \ + !list_is_head(pos, (head)); \ + pos = n, n = pos->next) + +/** + * list_for_each_prev_safe - iterate over a list backwards safe against removal of list entry + * @pos: the &struct list_head to use as a loop cursor. + * @n: another &struct list_head to use as temporary storage + * @head: the head for your list. + */ +#define list_for_each_prev_safe(pos, n, head) \ + for (pos = (head)->prev, n = pos->prev; \ + !list_is_head(pos, (head)); \ + pos = n, n = pos->prev) + +/** + * list_entry_is_head - test if the entry points to the head of the list + * @pos: the type * to cursor + * @head: the head for your list. + * @member: the name of the list_head within the struct. + */ +#define list_entry_is_head(pos, head, member) \ + (&pos->member == (head)) + +/** + * list_for_each_entry - iterate over list of given type + * @pos: the type * to use as a loop cursor. + * @head: the head for your list. + * @member: the name of the list_head within the struct. + */ +#define list_for_each_entry(pos, head, member) \ + for (pos = list_first_entry(head, typeof(*pos), member); \ + !list_entry_is_head(pos, head, member); \ + pos = list_next_entry(pos, member)) + +/** + * list_for_each_entry_reverse - iterate backwards over list of given type. + * @pos: the type * to use as a loop cursor. + * @head: the head for your list. + * @member: the name of the list_head within the struct. + */ +#define list_for_each_entry_reverse(pos, head, member) \ + for (pos = list_last_entry(head, typeof(*pos), member); \ + !list_entry_is_head(pos, head, member); \ + pos = list_prev_entry(pos, member)) + +/** + * list_prepare_entry - prepare a pos entry for use in list_for_each_entry_continue() + * @pos: the type * to use as a start point + * @head: the head of the list + * @member: the name of the list_head within the struct. + * + * Prepares a pos entry for use as a start point in list_for_each_entry_continue(). + */ +#define list_prepare_entry(pos, head, member) \ + ((pos) ? : list_entry(head, typeof(*pos), member)) + +/** + * list_for_each_entry_continue - continue iteration over list of given type + * @pos: the type * to use as a loop cursor. + * @head: the head for your list. + * @member: the name of the list_head within the struct. + * + * Continue to iterate over list of given type, continuing after + * the current position. + */ +#define list_for_each_entry_continue(pos, head, member) \ + for (pos = list_next_entry(pos, member); \ + !list_entry_is_head(pos, head, member); \ + pos = list_next_entry(pos, member)) + +/** + * list_for_each_entry_continue_reverse - iterate backwards from the given point + * @pos: the type * to use as a loop cursor. + * @head: the head for your list. + * @member: the name of the list_head within the struct. + * + * Start to iterate over list of given type backwards, continuing after + * the current position. + */ +#define list_for_each_entry_continue_reverse(pos, head, member) \ + for (pos = list_prev_entry(pos, member); \ + !list_entry_is_head(pos, head, member); \ + pos = list_prev_entry(pos, member)) + +/** + * list_for_each_entry_from - iterate over list of given type from the current point + * @pos: the type * to use as a loop cursor. + * @head: the head for your list. + * @member: the name of the list_head within the struct. + * + * Iterate over list of given type, continuing from current position. + */ +#define list_for_each_entry_from(pos, head, member) \ + for (; !list_entry_is_head(pos, head, member); \ + pos = list_next_entry(pos, member)) + +/** + * list_for_each_entry_from_reverse - iterate backwards over list of given type + * from the current point + * @pos: the type * to use as a loop cursor. + * @head: the head for your list. + * @member: the name of the list_head within the struct. + * + * Iterate backwards over list of given type, continuing from current position. + */ +#define list_for_each_entry_from_reverse(pos, head, member) \ + for (; !list_entry_is_head(pos, head, member); \ + pos = list_prev_entry(pos, member)) + +/** + * list_for_each_entry_safe - iterate over list of given type safe against removal of list entry + * @pos: the type * to use as a loop cursor. + * @n: another type * to use as temporary storage + * @head: the head for your list. + * @member: the name of the list_head within the struct. + */ +#define list_for_each_entry_safe(pos, n, head, member) \ + for (pos = list_first_entry(head, typeof(*pos), member), \ + n = list_next_entry(pos, member); \ + !list_entry_is_head(pos, head, member); \ + pos = n, n = list_next_entry(n, member)) + +/** + * list_for_each_entry_safe_continue - continue list iteration safe against removal + * @pos: the type * to use as a loop cursor. + * @n: another type * to use as temporary storage + * @head: the head for your list. + * @member: the name of the list_head within the struct. + * + * Iterate over list of given type, continuing after current point, + * safe against removal of list entry. + */ +#define list_for_each_entry_safe_continue(pos, n, head, member) \ + for (pos = list_next_entry(pos, member), \ + n = list_next_entry(pos, member); \ + !list_entry_is_head(pos, head, member); \ + pos = n, n = list_next_entry(n, member)) + +/** + * list_for_each_entry_safe_from - iterate over list from current point safe against removal + * @pos: the type * to use as a loop cursor. + * @n: another type * to use as temporary storage + * @head: the head for your list. + * @member: the name of the list_head within the struct. + * + * Iterate over list of given type from current point, safe against + * removal of list entry. + */ +#define list_for_each_entry_safe_from(pos, n, head, member) \ + for (n = list_next_entry(pos, member); \ + !list_entry_is_head(pos, head, member); \ + pos = n, n = list_next_entry(n, member)) + +/** + * list_for_each_entry_safe_reverse - iterate backwards over list safe against removal + * @pos: the type * to use as a loop cursor. + * @n: another type * to use as temporary storage + * @head: the head for your list. + * @member: the name of the list_head within the struct. + * + * Iterate backwards over list of given type, safe against removal + * of list entry. + */ +#define list_for_each_entry_safe_reverse(pos, n, head, member) \ + for (pos = list_last_entry(head, typeof(*pos), member), \ + n = list_prev_entry(pos, member); \ + !list_entry_is_head(pos, head, member); \ + pos = n, n = list_prev_entry(n, member)) + +/** + * list_safe_reset_next - reset a stale list_for_each_entry_safe loop + * @pos: the loop cursor used in the list_for_each_entry_safe loop + * @n: temporary storage used in list_for_each_entry_safe + * @member: the name of the list_head within the struct. + * + * list_safe_reset_next is not safe to use in general if the list may be + * modified concurrently (eg. the lock is dropped in the loop body). An + * exception to this is if the cursor element (pos) is pinned in the list, + * and list_safe_reset_next is called after re-taking the lock and before + * completing the current iteration of the loop body. + */ +#define list_safe_reset_next(pos, n, member) \ + n = list_next_entry(pos, member) + +/* + * Double linked lists with a single pointer list head. + * Mostly useful for hash tables where the two pointer list head is + * too wasteful. + * You lose the ability to access the tail in O(1). + */ + +#define HLIST_HEAD_INIT { .first = NULL } +#define HLIST_HEAD(name) struct hlist_head name = { .first = NULL } +#define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL) +static inline void INIT_HLIST_NODE(struct hlist_node *h) +{ + h->next = NULL; + h->pprev = NULL; +} + +/** + * hlist_unhashed - Has node been removed from list and reinitialized? + * @h: Node to be checked + * + * Not that not all removal functions will leave a node in unhashed + * state. For example, hlist_nulls_del_init_rcu() does leave the + * node in unhashed state, but hlist_nulls_del() does not. + */ +static inline int hlist_unhashed(const struct hlist_node *h) +{ + return !h->pprev; +} + +/** + * hlist_unhashed_lockless - Version of hlist_unhashed for lockless use + * @h: Node to be checked + * + * This variant of hlist_unhashed() must be used in lockless contexts + * to avoid potential load-tearing. The READ_ONCE() is paired with the + * various WRITE_ONCE() in hlist helpers that are defined below. + */ +static inline int hlist_unhashed_lockless(const struct hlist_node *h) +{ + return !READ_ONCE(h->pprev); +} + +/** + * hlist_empty - Is the specified hlist_head structure an empty hlist? + * @h: Structure to check. + */ +static inline int hlist_empty(const struct hlist_head *h) +{ + return !READ_ONCE(h->first); +} + +static inline void __hlist_del(struct hlist_node *n) +{ + struct hlist_node *next = n->next; + struct hlist_node **pprev = n->pprev; + + WRITE_ONCE(*pprev, next); + if (next) + WRITE_ONCE(next->pprev, pprev); +} + +/** + * hlist_del - Delete the specified hlist_node from its list + * @n: Node to delete. + * + * Note that this function leaves the node in hashed state. Use + * hlist_del_init() or similar instead to unhash @n. + */ +static inline void hlist_del(struct hlist_node *n) +{ + __hlist_del(n); + n->next = LIST_POISON1; + n->pprev = LIST_POISON2; +} + +/** + * hlist_del_init - Delete the specified hlist_node from its list and initialize + * @n: Node to delete. + * + * Note that this function leaves the node in unhashed state. + */ +static inline void hlist_del_init(struct hlist_node *n) +{ + if (!hlist_unhashed(n)) { + __hlist_del(n); + INIT_HLIST_NODE(n); + } +} + +/** + * hlist_add_head - add a new entry at the beginning of the hlist + * @n: new entry to be added + * @h: hlist head to add it after + * + * Insert a new entry after the specified head. + * This is good for implementing stacks. + */ +static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) +{ + struct hlist_node *first = h->first; + WRITE_ONCE(n->next, first); + if (first) + WRITE_ONCE(first->pprev, &n->next); + WRITE_ONCE(h->first, n); + WRITE_ONCE(n->pprev, &h->first); +} + +/** + * hlist_add_before - add a new entry before the one specified + * @n: new entry to be added + * @next: hlist node to add it before, which must be non-NULL + */ +static inline void hlist_add_before(struct hlist_node *n, + struct hlist_node *next) +{ + WRITE_ONCE(n->pprev, next->pprev); + WRITE_ONCE(n->next, next); + WRITE_ONCE(next->pprev, &n->next); + WRITE_ONCE(*(n->pprev), n); +} + +/** + * hlist_add_behind - add a new entry after the one specified + * @n: new entry to be added + * @prev: hlist node to add it after, which must be non-NULL + */ +static inline void hlist_add_behind(struct hlist_node *n, + struct hlist_node *prev) +{ + WRITE_ONCE(n->next, prev->next); + WRITE_ONCE(prev->next, n); + WRITE_ONCE(n->pprev, &prev->next); + + if (n->next) + WRITE_ONCE(n->next->pprev, &n->next); +} + +/** + * hlist_add_fake - create a fake hlist consisting of a single headless node + * @n: Node to make a fake list out of + * + * This makes @n appear to be its own predecessor on a headless hlist. + * The point of this is to allow things like hlist_del() to work correctly + * in cases where there is no list. + */ +static inline void hlist_add_fake(struct hlist_node *n) +{ + n->pprev = &n->next; +} + +/** + * hlist_fake: Is this node a fake hlist? + * @h: Node to check for being a self-referential fake hlist. + */ +static inline bool hlist_fake(struct hlist_node *h) +{ + return h->pprev == &h->next; +} + +/** + * hlist_is_singular_node - is node the only element of the specified hlist? + * @n: Node to check for singularity. + * @h: Header for potentially singular list. + * + * Check whether the node is the only node of the head without + * accessing head, thus avoiding unnecessary cache misses. + */ +static inline bool +hlist_is_singular_node(struct hlist_node *n, struct hlist_head *h) +{ + return !n->next && n->pprev == &h->first; +} + +/** + * hlist_move_list - Move an hlist + * @old: hlist_head for old list. + * @new: hlist_head for new list. + * + * Move a list from one list head to another. Fixup the pprev + * reference of the first entry if it exists. + */ +static inline void hlist_move_list(struct hlist_head *old, + struct hlist_head *new) +{ + new->first = old->first; + if (new->first) + new->first->pprev = &new->first; + old->first = NULL; +} + +#define hlist_entry(ptr, type, member) container_of(ptr,type,member) + +#define hlist_for_each(pos, head) \ + for (pos = (head)->first; pos ; pos = pos->next) + +#define hlist_for_each_safe(pos, n, head) \ + for (pos = (head)->first; pos && ({ n = pos->next; 1; }); \ + pos = n) + +#define hlist_entry_safe(ptr, type, member) \ + ({ typeof(ptr) ____ptr = (ptr); \ + ____ptr ? hlist_entry(____ptr, type, member) : NULL; \ + }) + +/** + * hlist_for_each_entry - iterate over list of given type + * @pos: the type * to use as a loop cursor. + * @head: the head for your list. + * @member: the name of the hlist_node within the struct. + */ +#define hlist_for_each_entry(pos, head, member) \ + for (pos = hlist_entry_safe((head)->first, typeof(*(pos)), member);\ + pos; \ + pos = hlist_entry_safe((pos)->member.next, typeof(*(pos)), member)) + +/** + * hlist_for_each_entry_continue - iterate over a hlist continuing after current point + * @pos: the type * to use as a loop cursor. + * @member: the name of the hlist_node within the struct. + */ +#define hlist_for_each_entry_continue(pos, member) \ + for (pos = hlist_entry_safe((pos)->member.next, typeof(*(pos)), member);\ + pos; \ + pos = hlist_entry_safe((pos)->member.next, typeof(*(pos)), member)) + +/** + * hlist_for_each_entry_from - iterate over a hlist continuing from current point + * @pos: the type * to use as a loop cursor. + * @member: the name of the hlist_node within the struct. + */ +#define hlist_for_each_entry_from(pos, member) \ + for (; pos; \ + pos = hlist_entry_safe((pos)->member.next, typeof(*(pos)), member)) + +/** + * hlist_for_each_entry_safe - iterate over list of given type safe against removal of list entry + * @pos: the type * to use as a loop cursor. + * @n: a &struct hlist_node to use as temporary storage + * @head: the head for your list. + * @member: the name of the hlist_node within the struct. + */ +#define hlist_for_each_entry_safe(pos, n, head, member) \ + for (pos = hlist_entry_safe((head)->first, typeof(*pos), member);\ + pos && ({ n = pos->member.next; 1; }); \ + pos = hlist_entry_safe(n, typeof(*pos), member)) + +#endif +Footer +© 2022 GitHub, Inc. +Footer navigation +Terms +Privacy +Security +Status +Docs +Contact GitHub +Pricing +API +Training +Blog +About +You have unread notifications diff --git a/internal/tmp/jam_tmp.go b/internal/tmp/jam_tmp.go index 6659763e..7b750461 100644 --- a/internal/tmp/jam_tmp.go +++ b/internal/tmp/jam_tmp.go @@ -1,5 +1,4 @@ // * In the process of taking useful code from here and moving to the `www` package - package tmp import ( diff --git a/internal/tmp/utils.go b/internal/tmp/utils.go index ee1116fa..8b586105 100644 --- a/internal/tmp/utils.go +++ b/internal/tmp/utils.go @@ -1,7 +1,6 @@ // * This file is not being used but saving as I have questions regarding the `isEmptyString` function // * Parse not required as is the case with the errorResponse as those should really live in the `www` package // * As there are function that depend on these utlities, I have refrained from deleting - package tmp import ( diff --git a/www/routes.go b/www/routes.go index 4e6c7717..67c58dd2 100644 --- a/www/routes.go +++ b/www/routes.go @@ -47,8 +47,8 @@ func (s Service) jamSessionHTML(path string) http.HandlerFunc { panic(err) } - // !I should be rendering a 404 page if there is an error - // !in this layer, but for an MVC this will do + // ! I should be rendering a 404 page if there is an error + // ! in this layer, but for an MVC this will do return func(w http.ResponseWriter, r *http.Request) { uid, err := s.parseUUID(w, r, "id") if err != nil { From e7620248f298e0e921d143a78771e5fd3cb0819b Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 16 Sep 2022 14:34:02 +0430 Subject: [PATCH 025/246] added prettierrc --- .prettierrc | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..d9f71486 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "trailingComma": "es5", + "tabWidth": 4, + "semi": true, + "singleQuote": true, + "bracketSpacing": true, + "bracketSameLine": true, + "arrowParens": "always", + "endOfLine": "lf", + "embeddedLanguageFormatting": "auto", + "singleAttributePerLine": true +} \ No newline at end of file From ffd38e0991fbe0c96f4d4432f120f0b4a17e919b Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 16 Sep 2022 11:30:52 +0100 Subject: [PATCH 026/246] go1.19 -> go1.18 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 26860196..31faa98a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/rog-golang-buddies/rapidmidiex -go 1.19 +go 1.18 require ( github.com/go-chi/chi v1.5.4 From a7b0fce88c9fd904e96f0c38e89b3675cc75491a Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 16 Sep 2022 11:31:10 +0100 Subject: [PATCH 027/246] contextKey now a type alias --- assets/src/index.js | 1 - www/www.go | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/assets/src/index.js b/assets/src/index.js index 91167ee8..4aeff6e2 100644 --- a/assets/src/index.js +++ b/assets/src/index.js @@ -24,7 +24,6 @@ const session = new Proxy({ id: "" }, { switch (prop) { case "id": obj[prop] = value; - // !should not be hard-coded but for MVC it's acceptable document.getElementById("session").textContent = window.location.href + "play/" + value; return true; diff --git a/www/www.go b/www/www.go index 2b491c13..d2e7e93d 100644 --- a/www/www.go +++ b/www/www.go @@ -6,13 +6,13 @@ import ( h "github.com/hyphengolang/prelude/http" ) -type contextKey struct{ string } +type contextKey string -func (c *contextKey) String() string { return "context value " + c.string } +// func (c *contextKey) String() string { return "context value " + c.string } var ( - roomKey = &contextKey{"ws-pool"} - upgradeKey = &contextKey{"http-upgrade"} + roomKey = contextKey("ws-pool") + upgradeKey = contextKey("http-upgrade") ) func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } From 44614a6c818852164562ba5fa3719b34ae1c14f1 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 16 Sep 2022 16:03:05 +0430 Subject: [PATCH 028/246] remove unnecessary request types --- cmd/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/run.go b/cmd/run.go index 4fec1cbd..447e5781 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -23,7 +23,7 @@ func run() error { c := cors.Options{ AllowedOrigins: []string{"http://localhost:" + port}, AllowCredentials: true, - AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete, http.MethodPatch}, + AllowedMethods: []string{http.MethodGet}, AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, } From 1175ae26ede484db0c33f8f96aabf1dbcd95bcaf Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 16 Sep 2022 16:06:19 +0430 Subject: [PATCH 029/246] fixed some typos --- internal/rmx.go | 38 +++++++++++++++++++------------------- www/routes.go | 6 +++--- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/internal/rmx.go b/internal/rmx.go index 0c6daea8..f489d906 100644 --- a/internal/rmx.go +++ b/internal/rmx.go @@ -1,6 +1,6 @@ package internal -type MessageTyp int +type MessageType int const ( Unknown = iota @@ -16,43 +16,43 @@ const ( NoteOff ) -func (t MessageTyp) String() string { +func (t MessageType) String() string { switch t { case Create: - return "Create" + return "CREATE" case Delete: - return "Delete" + return "DELETE" case Join: - return "Join" + return "JOIN" case Leave: - return "Leave" + return "LEAVE" case Message: - return "Message" + return "MESSAGE" case NoteOn: - return "NoteOn" + return "NOTE_ON" case NoteOff: - return "NoteOff" + return "NOTE_OFF" default: - return "Unknown" + return "UNKNOWN" } } -func (t *MessageTyp) UnmarshalJSON(b []byte) error { +func (t *MessageType) UnmarshalJSON(b []byte) error { switch s := string(b[1 : len(b)-1]); s { - case "Create": + case "CREATE": *t = Create - case "Delete": + case "DELETE": *t = Delete - case "Join": + case "JOIN": *t = Join - case "Leave": + case "LEAVE": *t = Leave - case "Message": + case "MESSAGE": *t = Message - case "NoteOn": + case "NOTE_ON": *t = NoteOn - case "NoteOff": + case "NOTE_OFF": *t = NoteOff default: *t = Unknown @@ -61,7 +61,7 @@ func (t *MessageTyp) UnmarshalJSON(b []byte) error { return nil } -func (t MessageTyp) MarshalJSON() ([]byte, error) { +func (t MessageType) MarshalJSON() ([]byte, error) { return []byte(`"` + t.String() + `"`), nil } diff --git a/www/routes.go b/www/routes.go index 67c58dd2..78662e71 100644 --- a/www/routes.go +++ b/www/routes.go @@ -67,9 +67,9 @@ func (s Service) jamSessionHTML(path string) http.HandlerFunc { func (s Service) handleJamSession() http.HandlerFunc { type response struct { - MessageTyp rmx.MessageTyp `json:"type"` - ID rmx.ID `json:"id"` - SessionID rmx.ID `json:"sessionId"` + MessageTyp rmx.MessageType `json:"type"` + ID rmx.ID `json:"id"` + SessionID rmx.ID `json:"sessionId"` } return func(w http.ResponseWriter, r *http.Request) { From 0b80b80d41b4582f5a9ec9ab5bc6bb3af875be83 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 16 Sep 2022 13:14:39 +0100 Subject: [PATCH 030/246] moved config inside cmd --- config.go => cmd/config.go | 4 ++-- cmd/run.go | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) rename config.go => cmd/config.go (93%) diff --git a/config.go b/cmd/config.go similarity index 93% rename from config.go rename to cmd/config.go index 09b703a9..e35f60d7 100644 --- a/config.go +++ b/cmd/config.go @@ -1,8 +1,8 @@ -package rmx +package main import "github.com/spf13/viper" -func LoadConfig() error { +func loadConfig() error { viper.SetConfigName("config") // name of config file (without extension) viper.SetConfigType("env") // REQUIRED if the config file does not have the extension in the name viper.AddConfigPath(".") // optionally look for config in the working directory diff --git a/cmd/run.go b/cmd/run.go index 4fec1cbd..9e264a82 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -18,6 +18,11 @@ import ( ) func run() error { + if err := loadConfig(); err != nil { + return err + } + + // port := viper.GetString("PORT") port := getEnv("PORT", "8888") c := cors.Options{ From 62d928e318e5abb09797681e3de56db862b4e296 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Fri, 16 Sep 2022 12:39:01 -0400 Subject: [PATCH 031/246] Server start log --- .vscode/launch.json | 2 +- cmd/run.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 5e3c488f..5528a866 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Launch Package", + "name": "Start Server", "type": "go", "request": "launch", "mode": "auto", diff --git a/cmd/run.go b/cmd/run.go index 447e5781..1bb1cba4 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -55,6 +55,7 @@ func run() error { g, gCtx := errgroup.WithContext(serverCtx) g.Go(func() error { // Run the server + log.Printf("App server starting on %s", server.Addr) return server.ListenAndServe() }) g.Go(func() error { From 48679e32b64b96722e64ebb6afe8c2097352dac0 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 16 Sep 2022 18:38:40 +0100 Subject: [PATCH 032/246] added a U/SUID type as wrapper/alternative to Google's uuid.UUID type --- internal/suid/suid.go | 29 +++++++++++++++++++++++++++++ internal/suid/types_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 internal/suid/suid.go create mode 100644 internal/suid/types_test.go diff --git a/internal/suid/suid.go b/internal/suid/suid.go new file mode 100644 index 00000000..e19c5fce --- /dev/null +++ b/internal/suid/suid.go @@ -0,0 +1,29 @@ +package suid + +import ( + "github.com/google/uuid" + suid "github.com/lithammer/shortuuid/v4" +) + +// ? The API for UUID/SUID is still in development +type UUID struct{ uuid.UUID } + +func NewUUID() UUID { return UUID{uuid.New()} } + +func (u UUID) ShortUUID() SUID { return SUID(suid.DefaultEncoder.Encode(u.UUID)) } + +type SUID string + +func NewSUID() SUID { return SUID(suid.New()) } + +func FromUUID(uid UUID) SUID { return SUID(suid.DefaultEncoder.Encode(uid.UUID)) } + +func ParseString(s string) (UUID, error) { + uid, err := suid.DefaultEncoder.Decode(s) + return UUID{uid}, err +} + +func (s SUID) UUID() (UUID, error) { + u, err := suid.DefaultEncoder.Decode(string(s)) + return UUID{u}, err +} diff --git a/internal/suid/types_test.go b/internal/suid/types_test.go new file mode 100644 index 00000000..eca5bdfa --- /dev/null +++ b/internal/suid/types_test.go @@ -0,0 +1,30 @@ +package suid + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestUUID(t *testing.T) { + uid := NewUUID() + + b, err := json.Marshal(uid.ShortUUID()) + if err != nil { + t.Fatal(err) + } + + var sid SUID + if err := json.NewDecoder(strings.NewReader(string(b))).Decode(&sid); err != nil { + t.Fatal(err) + } + + oid, err := sid.UUID() + if err != nil { + t.Fatal(err) + } + + if oid != uid { + t.Fatalf("expected: %s;got %s\n", uid, sid) + } +} From b8d709857b233adf43f35e774c4931b390aa61a2 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 16 Sep 2022 18:39:05 +0100 Subject: [PATCH 033/246] uuid.UUID -> rmx.UUID --- cmd/run.go | 7 +++--- internal/rmx.go | 2 +- www/{ws => internal}/client.go | 15 ++++++------- www/{ws => internal}/conn.go | 4 ++-- www/{ws => internal}/errors.go | 0 www/{ws => internal}/pool.go | 16 +++++++------- www/middleware.go | 2 +- www/routes.go | 39 ++++++++++++++++------------------ www/service.go | 14 ++++-------- www/www.go | 6 ++---- 10 files changed, 46 insertions(+), 59 deletions(-) rename www/{ws => internal}/client.go (67%) rename www/{ws => internal}/conn.go (85%) rename www/{ws => internal}/errors.go (100%) rename www/{ws => internal}/pool.go (80%) diff --git a/cmd/run.go b/cmd/run.go index 670ca8dd..47960750 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -18,11 +18,10 @@ import ( ) func run() error { - if err := loadConfig(); err != nil { - return err - } + // if err := loadConfig(); err != nil { + // return err + // } - // port := viper.GetString("PORT") port := getEnv("PORT", "8888") c := cors.Options{ diff --git a/internal/rmx.go b/internal/rmx.go index f489d906..e549eb36 100644 --- a/internal/rmx.go +++ b/internal/rmx.go @@ -65,5 +65,5 @@ func (t MessageType) MarshalJSON() ([]byte, error) { return []byte(`"` + t.String() + `"`), nil } -// ID with ability to encode to a URL friendly version +// ! ID with ability to encode to a URL friendly version type ID string diff --git a/www/ws/client.go b/www/internal/client.go similarity index 67% rename from www/ws/client.go rename to www/internal/client.go index 7f171b50..19ef2505 100644 --- a/www/ws/client.go +++ b/www/internal/client.go @@ -3,24 +3,23 @@ package websocket import ( "sync" - "github.com/google/uuid" - rmx "github.com/rog-golang-buddies/rapidmidiex/internal" + "github.com/rog-golang-buddies/rapidmidiex/internal/suid" ) type Client struct { mu sync.RWMutex - ps map[uuid.UUID]*Pool + ps map[suid.UUID]*Pool } var DefaultClient = &Client{ - ps: make(map[uuid.UUID]*Pool), + ps: make(map[suid.UUID]*Pool), } func NewClient() *Client { c := &Client{ - ps: make(map[uuid.UUID]*Pool), + ps: make(map[suid.UUID]*Pool), } return c @@ -32,7 +31,7 @@ func (c *Client) Close() error { return rmx.ErrTodo } -func (c *Client) NewPool() (uuid.UUID, error) { +func (c *Client) NewPool() (suid.UUID, error) { c.mu.Lock() defer c.mu.Unlock() @@ -43,7 +42,7 @@ func (c *Client) NewPool() (uuid.UUID, error) { return p.ID, nil } -func (c *Client) Get(uid uuid.UUID) (*Pool, error) { +func (c *Client) Get(uid suid.UUID) (*Pool, error) { c.mu.Lock() defer c.mu.Unlock() @@ -56,7 +55,7 @@ func (c *Client) Get(uid uuid.UUID) (*Pool, error) { return nil, ErrNoPool } -func (c *Client) Has(uid uuid.UUID) bool { +func (c *Client) Has(uid suid.UUID) bool { _, err := c.Get(uid) return err == nil } diff --git a/www/ws/conn.go b/www/internal/conn.go similarity index 85% rename from www/ws/conn.go rename to www/internal/conn.go index 1637d4ba..6f7fb92f 100644 --- a/www/ws/conn.go +++ b/www/internal/conn.go @@ -1,12 +1,12 @@ package websocket import ( - "github.com/google/uuid" "github.com/gorilla/websocket" + "github.com/rog-golang-buddies/rapidmidiex/internal/suid" ) type Conn struct { - ID uuid.UUID + ID suid.UUID rwc *websocket.Conn p *Pool diff --git a/www/ws/errors.go b/www/internal/errors.go similarity index 100% rename from www/ws/errors.go rename to www/internal/errors.go diff --git a/www/ws/pool.go b/www/internal/pool.go similarity index 80% rename from www/ws/pool.go rename to www/internal/pool.go index 9bc7fa1d..85ee5da7 100644 --- a/www/ws/pool.go +++ b/www/internal/pool.go @@ -4,8 +4,8 @@ import ( "net/http" "sync" - "github.com/google/uuid" "github.com/gorilla/websocket" + "github.com/rog-golang-buddies/rapidmidiex/internal/suid" // https://stackoverflow.com/questions/21362950/getting-a-slice-of-keys-from-a-map @@ -15,18 +15,18 @@ import ( type Pool struct { mu sync.RWMutex - ID uuid.UUID + ID suid.UUID MaxConn int - cs map[uuid.UUID]*Conn + cs map[suid.UUID]*Conn msgs chan any } func DefaultPool() *Pool { p := &Pool{ - ID: uuid.New(), + ID: suid.NewUUID(), MaxConn: 4, - cs: make(map[uuid.UUID]*Conn), + cs: make(map[suid.UUID]*Conn), msgs: make(chan any), } @@ -53,7 +53,7 @@ func (p *Pool) Size() int { return len(p.cs) } -func (p *Pool) Keys() []uuid.UUID { +func (p *Pool) Keys() []suid.UUID { p.mu.Lock() defer p.mu.Unlock() @@ -69,14 +69,14 @@ func (p *Pool) NewConn(w http.ResponseWriter, r *http.Request, u *websocket.Upgr return nil, err } - c := &Conn{uuid.New(), rwc, p} + c := &Conn{suid.NewUUID(), rwc, p} p.cs[c.ID] = c return c, nil } -func (p *Pool) Delete(uid uuid.UUID) { +func (p *Pool) Delete(uid suid.UUID) { p.mu.Lock() defer p.mu.Unlock() diff --git a/www/middleware.go b/www/middleware.go index 8047e012..595305be 100644 --- a/www/middleware.go +++ b/www/middleware.go @@ -6,7 +6,7 @@ import ( "github.com/gorilla/websocket" - ws "github.com/rog-golang-buddies/rapidmidiex/www/ws" + ws "github.com/rog-golang-buddies/rapidmidiex/www/internal" ) func (s Service) sessionPool(f http.HandlerFunc) http.HandlerFunc { diff --git a/www/routes.go b/www/routes.go index 78662e71..934bf27c 100644 --- a/www/routes.go +++ b/www/routes.go @@ -4,13 +4,12 @@ import ( "net/http" "github.com/go-chi/chi/v5/middleware" - "github.com/google/uuid" - suid "github.com/lithammer/shortuuid/v4" t "github.com/hyphengolang/prelude/template" rmx "github.com/rog-golang-buddies/rapidmidiex/internal" - ws "github.com/rog-golang-buddies/rapidmidiex/www/ws" + "github.com/rog-golang-buddies/rapidmidiex/internal/suid" + ws "github.com/rog-golang-buddies/rapidmidiex/www/internal" ) func (s Service) routes() { @@ -68,8 +67,8 @@ func (s Service) jamSessionHTML(path string) http.HandlerFunc { func (s Service) handleJamSession() http.HandlerFunc { type response struct { MessageTyp rmx.MessageType `json:"type"` - ID rmx.ID `json:"id"` - SessionID rmx.ID `json:"sessionId"` + ID suid.SUID `json:"id"` + SessionID suid.SUID `json:"sessionId"` } return func(w http.ResponseWriter, r *http.Request) { @@ -77,8 +76,8 @@ func (s Service) handleJamSession() http.HandlerFunc { defer func() { c.SendMessage(response{ MessageTyp: rmx.Leave, - ID: s.safeUUID(c.ID), - SessionID: s.safeUUID(c.Pool().ID), + ID: c.ID.ShortUUID(), + SessionID: c.Pool().ID.ShortUUID(), }) c.Close() @@ -86,8 +85,8 @@ func (s Service) handleJamSession() http.HandlerFunc { err := c.SendMessage(response{ MessageTyp: rmx.Join, - ID: s.safeUUID(c.ID), - SessionID: s.safeUUID(c.Pool().ID), + ID: c.ID.ShortUUID(), + SessionID: c.Pool().ID.ShortUUID(), }) if err != nil { @@ -95,9 +94,9 @@ func (s Service) handleJamSession() http.HandlerFunc { return } - // ?could the API be adjusted such that - // ?this for-loop only needs to read and - // ?never touch the code for writing + // ? could the API be adjusted such that + // ? this for-loop only needs to read and + // ? never touch the code for writing for { var n int if err := c.ReadJSON(&n); err != nil { @@ -115,7 +114,7 @@ func (s Service) handleJamSession() http.HandlerFunc { func (s Service) createSession() http.HandlerFunc { type response struct { - SessionID rmx.ID `json:"sessionId"` + SessionID suid.SUID `json:"sessionId"` } return func(w http.ResponseWriter, r *http.Request) { @@ -126,7 +125,7 @@ func (s Service) createSession() http.HandlerFunc { } v := response{ - SessionID: rmx.ID(suid.DefaultEncoder.Encode(uid)), + SessionID: suid.FromUUID(uid), } s.respond(w, r, v, http.StatusOK) @@ -135,8 +134,8 @@ func (s Service) createSession() http.HandlerFunc { func (s Service) getSessionData() http.HandlerFunc { type response struct { - SessionID rmx.ID `json:"sessionId"` - Users []rmx.ID `json:"users"` + SessionID suid.SUID `json:"sessionId"` + Users []suid.SUID `json:"users"` } return func(w http.ResponseWriter, r *http.Request) { @@ -146,7 +145,7 @@ func (s Service) getSessionData() http.HandlerFunc { return } - // !rename method as `Get` is undescriptive + // ! rename method as `Get` is undescriptive p, err := s.c.Get(uid) if err != nil { s.respond(w, r, err, http.StatusNotFound) @@ -154,10 +153,8 @@ func (s Service) getSessionData() http.HandlerFunc { } v := &response{ - SessionID: rmx.ID(suid.DefaultEncoder.Encode(p.ID)), - Users: rmx.FMap(p.Keys(), func(uid uuid.UUID) rmx.ID { - return rmx.ID(suid.DefaultEncoder.Encode(uid)) - }), + SessionID: p.ID.ShortUUID(), + Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), } s.respond(w, r, v, http.StatusOK) diff --git a/www/service.go b/www/service.go index 9b2100d5..67b80a9e 100644 --- a/www/service.go +++ b/www/service.go @@ -5,13 +5,11 @@ import ( "net/http" "github.com/go-chi/chi/v5" - "github.com/google/uuid" - suid "github.com/lithammer/shortuuid/v4" h "github.com/hyphengolang/prelude/http" - rmx "github.com/rog-golang-buddies/rapidmidiex/internal" - ws "github.com/rog-golang-buddies/rapidmidiex/www/ws" + "github.com/rog-golang-buddies/rapidmidiex/internal/suid" + ws "github.com/rog-golang-buddies/rapidmidiex/www/internal" ) type Service struct { @@ -37,10 +35,6 @@ func (s Service) fileServer(prefix string, dirname string) http.Handler { return h.FileServer(prefix, dirname) } -func (s Service) safeUUID(uid uuid.UUID) rmx.ID { - return rmx.ID(suid.DefaultEncoder.Encode(uid)) -} - -func (S Service) parseUUID(w http.ResponseWriter, r *http.Request, key string) (uuid.UUID, error) { - return suid.DefaultEncoder.Decode(chi.URLParam(r, key)) +func (S Service) parseUUID(w http.ResponseWriter, r *http.Request, key string) (suid.UUID, error) { + return suid.ParseString(chi.URLParam(r, key)) } diff --git a/www/www.go b/www/www.go index d2e7e93d..cf43ed25 100644 --- a/www/www.go +++ b/www/www.go @@ -8,11 +8,9 @@ import ( type contextKey string -// func (c *contextKey) String() string { return "context value " + c.string } - var ( - roomKey = contextKey("ws-pool") - upgradeKey = contextKey("http-upgrade") + roomKey = contextKey("rmx-fetch-pool") + upgradeKey = contextKey("rmx-upgrade-http") ) func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } From f5809ca753da6c5ab578a85607ef8fd09805c326 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 16 Sep 2022 18:45:36 +0100 Subject: [PATCH 034/246] refactor inside run function --- cmd/run.go | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index b18106a2..5be3bc17 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -24,6 +24,9 @@ func run() error { port := getEnv("PORT", "8888") + sCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + defer cancel() + c := cors.Options{ AllowedOrigins: []string{"http://localhost:" + port}, AllowCredentials: true, @@ -31,40 +34,26 @@ func run() error { AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, } - mux := chi.NewMux() - service := www.NewService(mux) - - h := cors.New(c).Handler(service) - - serverCtx, serverStop := signal.NotifyContext( - context.Background(), - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - defer serverStop() - - server := http.Server{ + srv := http.Server{ Addr: ":" + port, - Handler: h, + Handler: cors.New(c).Handler(www.NewService(chi.NewMux())), ReadTimeout: 10 * time.Second, // max time to read request from the client WriteTimeout: 10 * time.Second, // max time to write response to the client IdleTimeout: 120 * time.Second, // max time for connections using TCP Keep-Alive - BaseContext: func(_ net.Listener) context.Context { - return serverCtx - }, + BaseContext: func(_ net.Listener) context.Context { return sCtx }, } - g, gCtx := errgroup.WithContext(serverCtx) + g, gCtx := errgroup.WithContext(sCtx) + g.Go(func() error { // Run the server - log.Printf("App server starting on %s", server.Addr) - return server.ListenAndServe() + log.Printf("App server starting on %s", srv.Addr) + return srv.ListenAndServe() }) + g.Go(func() error { <-gCtx.Done() - return server.Shutdown(context.Background()) + return srv.Shutdown(context.Background()) }) if err := g.Wait(); err != nil { From 9dc2b0cc2b8ef939f10ea78ccfe05f491952b1fc Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 16 Sep 2022 20:29:14 +0100 Subject: [PATCH 035/246] inteface{} -> any --- www/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/service.go b/www/service.go index 67b80a9e..bb4cf6f1 100644 --- a/www/service.go +++ b/www/service.go @@ -27,7 +27,7 @@ func NewService(r chi.Router) *Service { return s } -func (s Service) respond(w http.ResponseWriter, r *http.Request, data interface{}, status int) { +func (s Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { h.Respond(w, r, data, status) } From c717b7a767f61a531ad57de9993e75a908b3c94a Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Fri, 16 Sep 2022 16:03:21 -0400 Subject: [PATCH 036/246] Reorg server and TUI cmds --- .vscode/launch.json | 2 +- cmd/{ => server}/config.go | 0 cmd/{ => server}/main.go | 0 cmd/{ => server}/run.go | 0 cmd/tui/main.go | 111 +++++++++++++++++++++++++++++++++++++ go.mod | 17 +++++- go.sum | 34 +++++++++++- 7 files changed, 160 insertions(+), 4 deletions(-) rename cmd/{ => server}/config.go (100%) rename cmd/{ => server}/main.go (100%) rename cmd/{ => server}/run.go (100%) create mode 100644 cmd/tui/main.go diff --git a/.vscode/launch.json b/.vscode/launch.json index 5528a866..d59a25cd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,7 @@ "type": "go", "request": "launch", "mode": "auto", - "program": "${workspaceFolder}/cmd", + "program": "${workspaceFolder}/cmd/server", "cwd": "${workspaceFolder}", "envFile": "${workspaceFolder}/.env" } diff --git a/cmd/config.go b/cmd/server/config.go similarity index 100% rename from cmd/config.go rename to cmd/server/config.go diff --git a/cmd/main.go b/cmd/server/main.go similarity index 100% rename from cmd/main.go rename to cmd/server/main.go diff --git a/cmd/run.go b/cmd/server/run.go similarity index 100% rename from cmd/run.go rename to cmd/server/run.go diff --git a/cmd/tui/main.go b/cmd/tui/main.go new file mode 100644 index 00000000..155ceaa8 --- /dev/null +++ b/cmd/tui/main.go @@ -0,0 +1,111 @@ +package main + +import ( + "fmt" + "os" + + tea "github.com/charmbracelet/bubbletea" +) + +type model struct { + choices []string // items on the to-do list + cursor int // which to-do list item our cursor is pointing at + selected map[int]struct{} // which to-do items are selected +} + +func initialModel() model { + return model{ + // Our to-do list is a grocery list + choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"}, + + // A map which indicates which choices are selected. We're using + // the map like a mathematical set. The keys refer to the indexes + // of the `choices` slice, above. + selected: make(map[int]struct{}), + } +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + + // Is it a key press? + case tea.KeyMsg: + + // Cool, what was the actual key pressed? + switch msg.String() { + + // These keys should exit the program. + case "ctrl+c", "q": + return m, tea.Quit + + // The "up" and "k" keys move the cursor up + case "up", "k": + if m.cursor > 0 { + m.cursor-- + } + + // The "down" and "j" keys move the cursor down + case "down", "j": + if m.cursor < len(m.choices)-1 { + m.cursor++ + } + + // The "enter" key and the spacebar (a literal space) toggle + // the selected state for the item that the cursor is pointing at. + case "enter", " ": + _, ok := m.selected[m.cursor] + if ok { + delete(m.selected, m.cursor) + } else { + m.selected[m.cursor] = struct{}{} + } + } + } + + // Return the updated model to the Bubble Tea runtime for processing. + // Note that we're not returning a command. + return m, nil +} + +func (m model) View() string { + // The header + s := "What should we buy at the market?\n\n" + + // Iterate over our choices + for i, choice := range m.choices { + + // Is the cursor pointing at this choice? + cursor := " " // no cursor + if m.cursor == i { + cursor = ">" // cursor! + } + + // Is this choice selected? + checked := " " // not selected + if _, ok := m.selected[i]; ok { + checked = "x" // selected! + } + + // Render the row + s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice) + } + + // The footer + s += "\nPress q to quit.\n" + + // Send the UI for rendering + return s +} + +func (m model) Init() tea.Cmd { + // Just return `nil`, which means "no I/O right now, please." + return nil +} + +func main() { + p := tea.NewProgram(initialModel()) + if err := p.Start(); err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod index 31faa98a..8650005b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/rog-golang-buddies/rapidmidiex go 1.18 require ( + github.com/charmbracelet/bubbletea v0.22.1 github.com/go-chi/chi v1.5.4 github.com/go-chi/chi/v5 v5.0.7 github.com/gobwas/ws v1.1.0 @@ -14,6 +15,20 @@ require ( golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde ) +require ( + github.com/containerd/console v1.0.3 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect +) + require ( github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/gobwas/httphead v0.1.0 // indirect @@ -32,7 +47,7 @@ require ( github.com/stretchr/testify v1.8.0 // indirect github.com/subosito/gotenv v1.3.0 // indirect golang.org/x/exp v0.0.0-20220914170420-dc92f8653013 - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.66.4 // indirect diff --git a/go.sum b/go.sum index 4ba254b9..6386fad8 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/charmbracelet/bubbletea v0.22.1 h1:z66q0LWdJNOWEH9zadiAIXp2GN1AWrwNXU8obVY9X24= +github.com/charmbracelet/bubbletea v0.22.1/go.mod h1:8/7hVvbPN6ZZPkczLiB8YpLkLJ0n7DMho5Wvfd2X1C0= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -46,6 +48,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -150,10 +154,28 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= +github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= @@ -164,6 +186,9 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= @@ -326,13 +351,18 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 5f4e6184c951f48532347a74700ebee33ab3076e Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 16 Sep 2022 21:07:47 +0100 Subject: [PATCH 037/246] mv run/loadConfig into main.go --- cmd/server/config.go | 20 ---------- cmd/server/main.go | 88 +++++++++++++++++++++++++++++++++++++++++++- cmd/server/run.go | 71 ----------------------------------- 3 files changed, 87 insertions(+), 92 deletions(-) delete mode 100644 cmd/server/config.go delete mode 100644 cmd/server/run.go diff --git a/cmd/server/config.go b/cmd/server/config.go deleted file mode 100644 index e35f60d7..00000000 --- a/cmd/server/config.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import "github.com/spf13/viper" - -func loadConfig() error { - viper.SetConfigName("config") // name of config file (without extension) - viper.SetConfigType("env") // REQUIRED if the config file does not have the extension in the name - viper.AddConfigPath(".") // optionally look for config in the working directory - - viper.SetDefault("PORT", "8080") // Set Default variables - - viper.AutomaticEnv() - - err := viper.ReadInConfig() // Find and read the config file - if err != nil { // Handle errors reading the config file - return err - } - - return nil -} diff --git a/cmd/server/main.go b/cmd/server/main.go index 2f468f2a..dd315933 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,9 +1,95 @@ package main -import "log" +import ( + "context" + "log" + "net" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/go-chi/chi/v5" + "github.com/rs/cors" + "github.com/spf13/viper" + "golang.org/x/sync/errgroup" + + "github.com/rog-golang-buddies/rapidmidiex/www" +) func main() { if err := run(); err != nil { log.Fatalln(err) } } + +func run() error { + // if err := loadConfig(); err != nil { + // return err + // } + + port := getEnv("PORT", "8888") + + sCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + defer cancel() + + c := cors.Options{ + AllowedOrigins: []string{"http://localhost:" + port}, + AllowCredentials: true, + AllowedMethods: []string{http.MethodGet}, + AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + } + + srv := http.Server{ + Addr: ":" + port, + Handler: cors.New(c).Handler(www.NewService(chi.NewMux())), + ReadTimeout: 10 * time.Second, // max time to read request from the client + WriteTimeout: 10 * time.Second, // max time to write response to the client + IdleTimeout: 120 * time.Second, // max time for connections using TCP Keep-Alive + BaseContext: func(_ net.Listener) context.Context { return sCtx }, + } + + g, gCtx := errgroup.WithContext(sCtx) + + g.Go(func() error { + // Run the server + log.Printf("App server starting on %s", srv.Addr) + return srv.ListenAndServe() + }) + + g.Go(func() error { + <-gCtx.Done() + return srv.Shutdown(context.Background()) + }) + + if err := g.Wait(); err != nil { + log.Printf("exit reason: %s \n", err) + } + + return nil +} + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} + +func loadConfig() error { + viper.SetConfigName("config") // name of config file (without extension) + viper.SetConfigType("env") // REQUIRED if the config file does not have the extension in the name + viper.AddConfigPath(".") // optionally look for config in the working directory + + viper.SetDefault("PORT", "8080") // Set Default variables + + viper.AutomaticEnv() + + err := viper.ReadInConfig() // Find and read the config file + if err != nil { // Handle errors reading the config file + return err + } + + return nil +} diff --git a/cmd/server/run.go b/cmd/server/run.go deleted file mode 100644 index 5be3bc17..00000000 --- a/cmd/server/run.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "context" - "log" - "net" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/go-chi/chi/v5" - "github.com/rs/cors" - "golang.org/x/sync/errgroup" - - "github.com/rog-golang-buddies/rapidmidiex/www" -) - -func run() error { - // if err := loadConfig(); err != nil { - // return err - // } - - port := getEnv("PORT", "8888") - - sCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) - defer cancel() - - c := cors.Options{ - AllowedOrigins: []string{"http://localhost:" + port}, - AllowCredentials: true, - AllowedMethods: []string{http.MethodGet}, - AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, - } - - srv := http.Server{ - Addr: ":" + port, - Handler: cors.New(c).Handler(www.NewService(chi.NewMux())), - ReadTimeout: 10 * time.Second, // max time to read request from the client - WriteTimeout: 10 * time.Second, // max time to write response to the client - IdleTimeout: 120 * time.Second, // max time for connections using TCP Keep-Alive - BaseContext: func(_ net.Listener) context.Context { return sCtx }, - } - - g, gCtx := errgroup.WithContext(sCtx) - - g.Go(func() error { - // Run the server - log.Printf("App server starting on %s", srv.Addr) - return srv.ListenAndServe() - }) - - g.Go(func() error { - <-gCtx.Done() - return srv.Shutdown(context.Background()) - }) - - if err := g.Wait(); err != nil { - log.Printf("exit reason: %s \n", err) - } - - return nil -} - -func getEnv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - return fallback -} From ac03d1876859d9183c1e41d73bca511316376057 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Sat, 17 Sep 2022 01:05:33 +0430 Subject: [PATCH 038/246] fixed dependencies --- go.mod | 2 +- go.sum | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 8650005b..c63c07af 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect + github.com/muesli/termenv v0.12.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect ) diff --git a/go.sum b/go.sum index 6386fad8..be6d59c0 100644 --- a/go.sum +++ b/go.sum @@ -176,6 +176,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= +github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= @@ -357,6 +359,7 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 77083456ee208c79cfd5d5c3560a937d2a725d63 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 17 Sep 2022 17:48:44 +0100 Subject: [PATCH 039/246] WIP: 5f4e618 mv run/loadConfig into main.go --- .gitignore | 3 + README.md | 10 ++- assets/src/index.js | 3 +- assets/src/play.js | 4 +- cli/main.go | 8 --- cmd/{server => }/main.go | 2 +- sqlc.yaml | 15 ----- {cmd/tui => ui/terminal}/main.go | 0 {pages => ui/www}/404.html | 0 {pages => ui/www}/index.html | 0 {pages => ui/www}/play.html | 0 www/middleware.go | 2 +- www/routes.go | 90 ++++++++++++++------------- www/service.go | 2 +- www/{internal => websocket}/client.go | 0 www/{internal => websocket}/conn.go | 0 www/{internal => websocket}/errors.go | 0 www/{internal => websocket}/pool.go | 0 18 files changed, 63 insertions(+), 76 deletions(-) delete mode 100644 cli/main.go rename cmd/{server => }/main.go (94%) delete mode 100644 sqlc.yaml rename {cmd/tui => ui/terminal}/main.go (100%) rename {pages => ui/www}/404.html (100%) rename {pages => ui/www}/index.html (100%) rename {pages => ui/www}/play.html (100%) rename www/{internal => websocket}/client.go (100%) rename www/{internal => websocket}/conn.go (100%) rename www/{internal => websocket}/errors.go (100%) rename www/{internal => websocket}/pool.go (100%) diff --git a/.gitignore b/.gitignore index 91faf9d5..d7fd8152 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ bin/ .DS_Store *.env cmd/**/config + +# NPM +node_modules diff --git a/README.md b/README.md index fe0e5cd4..7beee14f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# golang-template-repository + -# [Repo name] +# Rapidmidiex ## Description @@ -35,6 +35,10 @@ _Let people know what your project can do specifically. Provide context and add ## Installation + + + + _Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection._ ## Usage diff --git a/assets/src/index.js b/assets/src/index.js index 4aeff6e2..7b7572e2 100644 --- a/assets/src/index.js +++ b/assets/src/index.js @@ -10,7 +10,7 @@ app.innerHTML = ` document.querySelector("button").addEventListener("click", async e => { try { - const r = await fetch("/api/jam/create"); + const r = await fetch("/api/v1/jam/create"); const { sessionId } = await r.json(); session.id = sessionId; @@ -21,6 +21,7 @@ document.querySelector("button").addEventListener("click", async e => { const session = new Proxy({ id: "" }, { set(obj, prop, value) { + let v = 0; switch (prop) { case "id": obj[prop] = value; diff --git a/assets/src/play.js b/assets/src/play.js index 064e7509..ea393b0c 100644 --- a/assets/src/play.js +++ b/assets/src/play.js @@ -9,7 +9,7 @@ app.innerHTML = `
          `; -const ws = new WebSocket(websocketUrl(`/jam/${sessionId()}`)); +const ws = new WebSocket(websocketUrl(`/api/v1/jam/${sessionId()}/ws`)); ws.addEventListener("open", e => { document.querySelector("button").disabled = false; @@ -41,7 +41,7 @@ document.querySelector("button").addEventListener("click", e => { }); async function newUserJoined() { - const r = await fetch(`/api/jam/${sessionId()}`); + const r = await fetch(`/api/v1/jam/${sessionId()}`); const { users } = await r.json(); // ^proxy Array that updates the list in the DOM diff --git a/cli/main.go b/cli/main.go deleted file mode 100644 index 63e4dc31..00000000 --- a/cli/main.go +++ /dev/null @@ -1,8 +0,0 @@ -package main - -import "fmt" - -func main() { - // Feel free to delete this file. - fmt.Println("Hello Gophers") -} diff --git a/cmd/server/main.go b/cmd/main.go similarity index 94% rename from cmd/server/main.go rename to cmd/main.go index dd315933..472718c3 100644 --- a/cmd/server/main.go +++ b/cmd/main.go @@ -35,7 +35,7 @@ func run() error { defer cancel() c := cors.Options{ - AllowedOrigins: []string{"http://localhost:" + port}, + AllowedOrigins: []string{"http://localhost:" + port, "http://localhost:5173"}, // ? band-aid, needs to change to a flag AllowCredentials: true, AllowedMethods: []string{http.MethodGet}, AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, diff --git a/sqlc.yaml b/sqlc.yaml deleted file mode 100644 index 540906dc..00000000 --- a/sqlc.yaml +++ /dev/null @@ -1,15 +0,0 @@ -version: "2" -sql: - - schema: "api/internal/db/user/schema.sql" - queries: "api/internal/db/user/query.sql" - engine: "mysql" - gen: - go: - package: "user" - out: "api/internal/db/user" - emit_db_tags: true - emit_prepared_queries: true - emit_empty_slices: true - emit_params_struct_pointers: true - emit_json_tags: true - json_tags_case_style: "snake" diff --git a/cmd/tui/main.go b/ui/terminal/main.go similarity index 100% rename from cmd/tui/main.go rename to ui/terminal/main.go diff --git a/pages/404.html b/ui/www/404.html similarity index 100% rename from pages/404.html rename to ui/www/404.html diff --git a/pages/index.html b/ui/www/index.html similarity index 100% rename from pages/index.html rename to ui/www/index.html diff --git a/pages/play.html b/ui/www/play.html similarity index 100% rename from pages/play.html rename to ui/www/play.html diff --git a/www/middleware.go b/www/middleware.go index 595305be..e968c3e6 100644 --- a/www/middleware.go +++ b/www/middleware.go @@ -6,7 +6,7 @@ import ( "github.com/gorilla/websocket" - ws "github.com/rog-golang-buddies/rapidmidiex/www/internal" + ws "github.com/rog-golang-buddies/rapidmidiex/www/websocket" ) func (s Service) sessionPool(f http.HandlerFunc) http.HandlerFunc { diff --git a/www/routes.go b/www/routes.go index 934bf27c..00161a0a 100644 --- a/www/routes.go +++ b/www/routes.go @@ -9,59 +9,26 @@ import ( rmx "github.com/rog-golang-buddies/rapidmidiex/internal" "github.com/rog-golang-buddies/rapidmidiex/internal/suid" - ws "github.com/rog-golang-buddies/rapidmidiex/www/internal" + ws "github.com/rog-golang-buddies/rapidmidiex/www/websocket" ) func (s Service) routes() { // middleware s.r.Use(middleware.Logger) - // http + // v0 s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) - s.r.Get("/", s.indexHTML("pages/index.html")) - s.r.Get("/play/{id}", s.jamSessionHTML("pages/play.html")) + s.r.Get("/", s.indexHTML("ui/www/index.html")) + s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) - // api - s.r.Get("/api/jam/create", s.createSession()) - s.r.Get("/api/jam/{id}", s.getSessionData()) + // s.r.Get("/api/v0/jam/create", s.createSession()) + // s.r.Get("/api/v0/jam/{id}", s.getSessionData()) + // s.r.HandleFunc("api/v0/jam/{id}", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) - // ws - s.r.HandleFunc("/jam/{id}", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) -} - -func (s Service) indexHTML(path string) http.HandlerFunc { - render, err := t.Render(path) - if err != nil { - panic(err) - } - - return func(w http.ResponseWriter, r *http.Request) { - render(w, r, nil) - } -} - -func (s Service) jamSessionHTML(path string) http.HandlerFunc { - render, err := t.Render(path) - if err != nil { - panic(err) - } - - // ! I should be rendering a 404 page if there is an error - // ! in this layer, but for an MVC this will do - return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r, "id") - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - if _, err = s.c.Get(uid); err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - render(w, r, nil) - } + // v1 + s.r.Get("/api/v1/jam/create", s.createSession()) + s.r.Get("/api/v1/jam/{id}", s.getSessionData()) + s.r.HandleFunc("/api/v1/jam/{id}/ws", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) } func (s Service) handleJamSession() http.HandlerFunc { @@ -160,3 +127,38 @@ func (s Service) getSessionData() http.HandlerFunc { s.respond(w, r, v, http.StatusOK) } } + +func (s Service) indexHTML(path string) http.HandlerFunc { + render, err := t.Render(path) + if err != nil { + panic(err) + } + + return func(w http.ResponseWriter, r *http.Request) { + render(w, r, nil) + } +} + +func (s Service) jamSessionHTML(path string) http.HandlerFunc { + render, err := t.Render(path) + if err != nil { + panic(err) + } + + // ! I should be rendering a 404 page if there is an error + // ! in this layer, but for an MVC this will do + return func(w http.ResponseWriter, r *http.Request) { + uid, err := s.parseUUID(w, r, "id") + if err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + if _, err = s.c.Get(uid); err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + render(w, r, nil) + } +} diff --git a/www/service.go b/www/service.go index bb4cf6f1..7e2467ba 100644 --- a/www/service.go +++ b/www/service.go @@ -9,7 +9,7 @@ import ( h "github.com/hyphengolang/prelude/http" "github.com/rog-golang-buddies/rapidmidiex/internal/suid" - ws "github.com/rog-golang-buddies/rapidmidiex/www/internal" + ws "github.com/rog-golang-buddies/rapidmidiex/www/websocket" ) type Service struct { diff --git a/www/internal/client.go b/www/websocket/client.go similarity index 100% rename from www/internal/client.go rename to www/websocket/client.go diff --git a/www/internal/conn.go b/www/websocket/conn.go similarity index 100% rename from www/internal/conn.go rename to www/websocket/conn.go diff --git a/www/internal/errors.go b/www/websocket/errors.go similarity index 100% rename from www/internal/errors.go rename to www/websocket/errors.go diff --git a/www/internal/pool.go b/www/websocket/pool.go similarity index 100% rename from www/internal/pool.go rename to www/websocket/pool.go From 573cdcb105c11175b285ce0736fd91fdd749195b Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 17 Sep 2022 17:50:33 +0100 Subject: [PATCH 040/246] v0 is depreciated --- www/routes.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/www/routes.go b/www/routes.go index 00161a0a..6ba7b547 100644 --- a/www/routes.go +++ b/www/routes.go @@ -20,10 +20,9 @@ func (s Service) routes() { s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) s.r.Get("/", s.indexHTML("ui/www/index.html")) s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) - - // s.r.Get("/api/v0/jam/create", s.createSession()) - // s.r.Get("/api/v0/jam/{id}", s.getSessionData()) - // s.r.HandleFunc("api/v0/jam/{id}", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) + // s.r.Get("/api/jam/create", s.createSession()) + // s.r.Get("/api/jam/{id}", s.getSessionData()) + // s.r.HandleFunc("/jam/{id}", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) // v1 s.r.Get("/api/v1/jam/create", s.createSession()) From 792c5ae0efec0c623b83d69df8262a0e2384229e Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 17 Sep 2022 19:36:08 +0100 Subject: [PATCH 041/246] suid test file change --- internal/suid/{types_test.go => suid_test.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/suid/{types_test.go => suid_test.go} (100%) diff --git a/internal/suid/types_test.go b/internal/suid/suid_test.go similarity index 100% rename from internal/suid/types_test.go rename to internal/suid/suid_test.go From c19f6fea38f53861edcc7270d6c426cadcf084b5 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Sat, 17 Sep 2022 23:22:59 +0430 Subject: [PATCH 042/246] added new id generation tool --- internal/ruid/ruid.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 internal/ruid/ruid.go diff --git a/internal/ruid/ruid.go b/internal/ruid/ruid.go new file mode 100644 index 00000000..c0de4e1b --- /dev/null +++ b/internal/ruid/ruid.go @@ -0,0 +1,24 @@ +package ruid + +import ( + "crypto/rand" + "io" +) + +type RUID string + +var table = [...]byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'} + +func New(length int) (string, error) { + b := make([]byte, length) + n, err := io.ReadAtLeast(rand.Reader, b, length) + if n != length { + return "", err + } + for i := 0; i < len(b); i++ { + b[i] = table[int(b[i])%len(table)] + } + return string(b), nil +} + +func (r RUID) String() string { return string(r) } From be28c5819d9f189c106210f35380c792263ab8be Mon Sep 17 00:00:00 2001 From: pmoieni Date: Mon, 19 Sep 2022 00:04:05 +0430 Subject: [PATCH 043/246] added list sessions handler --- www/routes.go | 49 ++++++++++++++++++++++++++++++++++------- www/websocket/client.go | 8 +++++++ 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/www/routes.go b/www/routes.go index 6ba7b547..38c93849 100644 --- a/www/routes.go +++ b/www/routes.go @@ -12,7 +12,15 @@ import ( ws "github.com/rog-golang-buddies/rapidmidiex/www/websocket" ) -func (s Service) routes() { +type ( + session struct { + Name string `json:"name,omitempty"` + SessionID suid.SUID `json:"sessionId,omitempty"` + Users []suid.SUID `json:"users,omitempty"` + } +) + +func (s *Service) routes() { // middleware s.r.Use(middleware.Logger) @@ -25,12 +33,13 @@ func (s Service) routes() { // s.r.HandleFunc("/jam/{id}", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) // v1 + s.r.Get("/api/v1/jam", s.listSessions()) s.r.Get("/api/v1/jam/create", s.createSession()) s.r.Get("/api/v1/jam/{id}", s.getSessionData()) - s.r.HandleFunc("/api/v1/jam/{id}/ws", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) + s.r.Get("/api/v1/jam/{id}/ws", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) } -func (s Service) handleJamSession() http.HandlerFunc { +func (s *Service) handleJamSession() http.HandlerFunc { type response struct { MessageTyp rmx.MessageType `json:"type"` ID suid.SUID `json:"id"` @@ -78,7 +87,7 @@ func (s Service) handleJamSession() http.HandlerFunc { } } -func (s Service) createSession() http.HandlerFunc { +func (s *Service) createSession() http.HandlerFunc { type response struct { SessionID suid.SUID `json:"sessionId"` } @@ -98,7 +107,7 @@ func (s Service) createSession() http.HandlerFunc { } } -func (s Service) getSessionData() http.HandlerFunc { +func (s *Service) getSessionData() http.HandlerFunc { type response struct { SessionID suid.SUID `json:"sessionId"` Users []suid.SUID `json:"users"` @@ -111,7 +120,7 @@ func (s Service) getSessionData() http.HandlerFunc { return } - // ! rename method as `Get` is undescriptive + // ! rename method as `Get` is nondescriptive p, err := s.c.Get(uid) if err != nil { s.respond(w, r, err, http.StatusNotFound) @@ -127,7 +136,31 @@ func (s Service) getSessionData() http.HandlerFunc { } } -func (s Service) indexHTML(path string) http.HandlerFunc { +func (s *Service) listSessions() http.HandlerFunc { + type response struct { + Sessions []session `json:"sessions"` + } + + return func(w http.ResponseWriter, r *http.Request) { + pl := s.c.List() + + sl := make([]session, 0, len(pl)) + for _, p := range pl { + sl = append(sl, session{ + Name: "", // name not implemented yet + SessionID: p.ID.ShortUUID(), + }) + } + + v := &response{ + Sessions: sl, + } + + s.respond(w, r, v, http.StatusOK) + } +} + +func (s *Service) indexHTML(path string) http.HandlerFunc { render, err := t.Render(path) if err != nil { panic(err) @@ -138,7 +171,7 @@ func (s Service) indexHTML(path string) http.HandlerFunc { } } -func (s Service) jamSessionHTML(path string) http.HandlerFunc { +func (s *Service) jamSessionHTML(path string) http.HandlerFunc { render, err := t.Render(path) if err != nil { panic(err) diff --git a/www/websocket/client.go b/www/websocket/client.go index 19ef2505..d5487030 100644 --- a/www/websocket/client.go +++ b/www/websocket/client.go @@ -5,6 +5,7 @@ import ( rmx "github.com/rog-golang-buddies/rapidmidiex/internal" "github.com/rog-golang-buddies/rapidmidiex/internal/suid" + "golang.org/x/exp/maps" ) type Client struct { @@ -59,3 +60,10 @@ func (c *Client) Has(uid suid.UUID) bool { _, err := c.Get(uid) return err == nil } + +func (c *Client) List() []*Pool { + c.mu.Lock() + defer c.mu.Unlock() + + return maps.Values(c.ps) +} From b4ae5851ea638dad7ada8ac5f592d17f64ce9226 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sun, 18 Sep 2022 20:38:19 +0100 Subject: [PATCH 044/246] tmp rm of assets folder --- www/routes.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/www/routes.go b/www/routes.go index 6ba7b547..ec5a24e5 100644 --- a/www/routes.go +++ b/www/routes.go @@ -16,10 +16,12 @@ func (s Service) routes() { // middleware s.r.Use(middleware.Logger) + // temporary static files + // s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) + // s.r.Get("/", s.indexHTML("ui/www/index.html")) + // s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) + // v0 - s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) - s.r.Get("/", s.indexHTML("ui/www/index.html")) - s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) // s.r.Get("/api/jam/create", s.createSession()) // s.r.Get("/api/jam/{id}", s.getSessionData()) // s.r.HandleFunc("/jam/{id}", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) From a0c28add67dd9eed34e6d0beb857bd3b70a4cb6d Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Mon, 19 Sep 2022 11:02:05 -0400 Subject: [PATCH 045/246] Refactor REST and ws routes --- .vscode/launch.json | 4 ++-- assets/src/index.js | 43 +++++++++++++++++++++++++++++-------------- assets/src/play.js | 32 +++++++++++++++----------------- www/routes.go | 8 +++++--- 4 files changed, 51 insertions(+), 36 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index d59a25cd..9386cbc2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,9 +9,9 @@ "type": "go", "request": "launch", "mode": "auto", - "program": "${workspaceFolder}/cmd/server", + "program": "${workspaceFolder}/cmd", "cwd": "${workspaceFolder}", - "envFile": "${workspaceFolder}/.env" + "envFile": "${workspaceFolder}/cmd/.env" } ] } diff --git a/assets/src/index.js b/assets/src/index.js index 7b7572e2..0e4978d8 100644 --- a/assets/src/index.js +++ b/assets/src/index.js @@ -1,4 +1,4 @@ -const app = document.getElementById("app"); +const app = document.getElementById('app'); app.innerHTML = `

          Welcome, Home

          @@ -8,9 +8,14 @@ app.innerHTML = `
          `; -document.querySelector("button").addEventListener("click", async e => { +document.querySelector('button').addEventListener('click', async (e) => { try { - const r = await fetch("/api/v1/jam/create"); + const r = await fetch('/api/v1/jam', { + method: 'POST', + body: { + // TODO: Add any info from UI + }, + }); const { sessionId } = await r.json(); session.id = sessionId; @@ -19,16 +24,26 @@ document.querySelector("button").addEventListener("click", async e => { } }); -const session = new Proxy({ id: "" }, { - set(obj, prop, value) { - let v = 0; - switch (prop) { - case "id": - obj[prop] = value; - document.getElementById("session").textContent = window.location.href + "play/" + value; - return true; +const session = new Proxy( + { id: '' }, + { + set(obj, prop, value) { + let v = 0; + switch (prop) { + case 'id': + obj[prop] = value; + const url = `${window.location.href}play/${value}`; + const anchor = document.createElement('a'); + anchor.href = url; + anchor.innerText = url; + anchor.target = '_blank'; + document.getElementById('session').appendChild(anchor); - default: return false; - } + return true; + + default: + return false; + } + }, } -}); \ No newline at end of file +); diff --git a/assets/src/play.js b/assets/src/play.js index ea393b0c..b0558602 100644 --- a/assets/src/play.js +++ b/assets/src/play.js @@ -1,6 +1,6 @@ -import { websocketUrl, sessionId } from "./utils.js"; +import { websocketUrl, sessionId } from './utils.js'; -const app = document.getElementById("app"); +const app = document.getElementById('app'); app.innerHTML = `

          Welcome to the JAM Session

          @@ -9,34 +9,34 @@ app.innerHTML = `
            `; -const ws = new WebSocket(websocketUrl(`/api/v1/jam/${sessionId()}/ws`)); +const ws = new WebSocket(websocketUrl(`/ws`)); -ws.addEventListener("open", e => { - document.querySelector("button").disabled = false; +ws.addEventListener('open', (e) => { + document.querySelector('button').disabled = false; - alert("web socket has opened"); + alert('web socket has opened'); }); -ws.addEventListener("message", async e => { +ws.addEventListener('message', async (e) => { const { type, id } = JSON.parse(e.data); switch (type.toLowerCase()) { - case "join": + case 'join': newUserJoined(); break; - case "leave": + case 'leave': document.getElementById(id).remove(); break; } }); -ws.addEventListener("error", e => { +ws.addEventListener('error', (e) => { console.error(e); - document.querySelector("button").disabled = true; + document.querySelector('button').disabled = true; }); -document.querySelector("button").addEventListener("click", e => { +document.querySelector('button').addEventListener('click', (e) => { ws.send(1); }); @@ -47,13 +47,11 @@ async function newUserJoined() { // ^proxy Array that updates the list in the DOM const items = []; for (const id of users) { - const li = document.createElement("li"); + const li = document.createElement('li'); li.textContent = `${id} has joined`; li.id = id; items.push(li); } - document - .querySelector(`[aria-label="users"]`) - .replaceChildren(...items); -} \ No newline at end of file + document.querySelector(`[aria-label="users"]`).replaceChildren(...items); +} diff --git a/www/routes.go b/www/routes.go index 38c93849..4a95d829 100644 --- a/www/routes.go +++ b/www/routes.go @@ -32,11 +32,13 @@ func (s *Service) routes() { // s.r.Get("/api/jam/{id}", s.getSessionData()) // s.r.HandleFunc("/jam/{id}", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) - // v1 + // REST v1 s.r.Get("/api/v1/jam", s.listSessions()) - s.r.Get("/api/v1/jam/create", s.createSession()) + s.r.Post("/api/v1/jam", s.createSession()) s.r.Get("/api/v1/jam/{id}", s.getSessionData()) - s.r.Get("/api/v1/jam/{id}/ws", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) + + // Websocket + s.r.Get("/ws", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) } func (s *Service) handleJamSession() http.HandlerFunc { From 40a6d2963d94b079f878c828cbc07a6ddbb8769e Mon Sep 17 00:00:00 2001 From: adoublef Date: Mon, 19 Sep 2022 16:33:11 +0100 Subject: [PATCH 046/246] routes change --- www/routes.go | 46 +++++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/www/routes.go b/www/routes.go index 3f0083c1..952eecff 100644 --- a/www/routes.go +++ b/www/routes.go @@ -12,14 +12,6 @@ import ( ws "github.com/rog-golang-buddies/rapidmidiex/www/websocket" ) -type ( - session struct { - Name string `json:"name,omitempty"` - SessionID suid.SUID `json:"sessionId,omitempty"` - Users []suid.SUID `json:"users,omitempty"` - } -) - func (s *Service) routes() { // middleware s.r.Use(middleware.Logger) @@ -90,9 +82,9 @@ func (s *Service) handleJamSession() http.HandlerFunc { } func (s *Service) createSession() http.HandlerFunc { - type response struct { - SessionID suid.SUID `json:"sessionId"` - } + // type response struct { + // SessionID suid.SUID `json:"sessionId"` + // } return func(w http.ResponseWriter, r *http.Request) { uid, err := s.c.NewPool() @@ -101,7 +93,7 @@ func (s *Service) createSession() http.HandlerFunc { return } - v := response{ + v := Session{ SessionID: suid.FromUUID(uid), } @@ -110,10 +102,10 @@ func (s *Service) createSession() http.HandlerFunc { } func (s *Service) getSessionData() http.HandlerFunc { - type response struct { - SessionID suid.SUID `json:"sessionId"` - Users []suid.SUID `json:"users"` - } + // type response struct { + // SessionID suid.SUID `json:"sessionId"` + // Users []suid.SUID `json:"users"` + // } return func(w http.ResponseWriter, r *http.Request) { uid, err := s.parseUUID(w, r, "id") @@ -129,7 +121,7 @@ func (s *Service) getSessionData() http.HandlerFunc { return } - v := &response{ + v := &Session{ SessionID: p.ID.ShortUUID(), Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), } @@ -140,22 +132,22 @@ func (s *Service) getSessionData() http.HandlerFunc { func (s *Service) listSessions() http.HandlerFunc { type response struct { - Sessions []session `json:"sessions"` + Sessions []Session `json:"sessions"` } return func(w http.ResponseWriter, r *http.Request) { - pl := s.c.List() + // pl := s.c.List() - sl := make([]session, 0, len(pl)) - for _, p := range pl { - sl = append(sl, session{ - Name: "", // name not implemented yet - SessionID: p.ID.ShortUUID(), - }) - } + // sl := make([]session, 0, len(pl)) + // for _, p := range pl { + // sl = append(sl, session{ + // Name: "", // name not implemented yet + // SessionID: p.ID.ShortUUID(), + // }) + // } v := &response{ - Sessions: sl, + Sessions: rmx.FMap(s.c.List(), func(p *ws.Pool) Session { return Session{SessionID: p.ID.ShortUUID()} }), } s.respond(w, r, v, http.StatusOK) From e30b8982b1bdb666dcb0b6858447e305d281de24 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Mon, 19 Sep 2022 20:11:04 +0430 Subject: [PATCH 047/246] change sessionData handler syntax --- www/routes.go | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/www/routes.go b/www/routes.go index 4a95d829..b8121ca6 100644 --- a/www/routes.go +++ b/www/routes.go @@ -35,7 +35,7 @@ func (s *Service) routes() { // REST v1 s.r.Get("/api/v1/jam", s.listSessions()) s.r.Post("/api/v1/jam", s.createSession()) - s.r.Get("/api/v1/jam/{id}", s.getSessionData()) + s.r.Get("/api/v1/jam/{id}", s.getSessionData) // Websocket s.r.Get("/ws", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) @@ -109,33 +109,26 @@ func (s *Service) createSession() http.HandlerFunc { } } -func (s *Service) getSessionData() http.HandlerFunc { - type response struct { - SessionID suid.SUID `json:"sessionId"` - Users []suid.SUID `json:"users"` +func (s *Service) getSessionData(w http.ResponseWriter, r *http.Request) { + uid, err := s.parseUUID(w, r, "id") + if err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return } - return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r, "id") - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - // ! rename method as `Get` is nondescriptive - p, err := s.c.Get(uid) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - v := &response{ - SessionID: p.ID.ShortUUID(), - Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), - } + // ! rename method as `Get` is nondescriptive + p, err := s.c.Get(uid) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } - s.respond(w, r, v, http.StatusOK) + v := &session{ + SessionID: p.ID.ShortUUID(), + Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), } + + s.respond(w, r, v, http.StatusOK) } func (s *Service) listSessions() http.HandlerFunc { From 7e694ae83c782b6681c75f80a7047c467b333957 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Mon, 19 Sep 2022 22:40:21 +0430 Subject: [PATCH 048/246] changed www to api --- www/www.go => api/api.go | 2 +- api/errors.go | 11 +++++++++++ {www => api}/middleware.go | 4 ++-- {www => api}/routes.go | 4 ++-- {www => api}/service.go | 4 ++-- {www => api}/websocket/client.go | 0 {www => api}/websocket/conn.go | 0 {www => api}/websocket/errors.go | 0 {www => api}/websocket/pool.go | 0 cmd/main.go | 4 ++-- internal/tmp/jam_tmp.go | 12 ++++++------ internal/tmp/utils.go | 4 ++-- www/errors.go | 11 ----------- 13 files changed, 28 insertions(+), 28 deletions(-) rename www/www.go => api/api.go (96%) create mode 100644 api/errors.go rename {www => api}/middleware.go (93%) rename {www => api}/routes.go (98%) rename {www => api}/service.go (91%) rename {www => api}/websocket/client.go (100%) rename {www => api}/websocket/conn.go (100%) rename {www => api}/websocket/errors.go (100%) rename {www => api}/websocket/pool.go (100%) delete mode 100644 www/errors.go diff --git a/www/www.go b/api/api.go similarity index 96% rename from www/www.go rename to api/api.go index cf43ed25..f6c8d433 100644 --- a/www/www.go +++ b/api/api.go @@ -1,4 +1,4 @@ -package www +package api import ( "net/http" diff --git a/api/errors.go b/api/errors.go new file mode 100644 index 00000000..d8ad7b83 --- /dev/null +++ b/api/errors.go @@ -0,0 +1,11 @@ +package api + +import ( + "errors" +) + +var ( + ErrNoCookie = errors.New("api: cookie not found") + ErrSessionNotFound = errors.New("api: session not found") + ErrSessionExists = errors.New("api: session already exists") +) diff --git a/www/middleware.go b/api/middleware.go similarity index 93% rename from www/middleware.go rename to api/middleware.go index e968c3e6..8962300a 100644 --- a/www/middleware.go +++ b/api/middleware.go @@ -1,4 +1,4 @@ -package www +package api import ( "context" @@ -6,7 +6,7 @@ import ( "github.com/gorilla/websocket" - ws "github.com/rog-golang-buddies/rapidmidiex/www/websocket" + ws "github.com/rog-golang-buddies/rapidmidiex/api/websocket" ) func (s Service) sessionPool(f http.HandlerFunc) http.HandlerFunc { diff --git a/www/routes.go b/api/routes.go similarity index 98% rename from www/routes.go rename to api/routes.go index b8121ca6..b6c142f5 100644 --- a/www/routes.go +++ b/api/routes.go @@ -1,4 +1,4 @@ -package www +package api import ( "net/http" @@ -7,9 +7,9 @@ import ( t "github.com/hyphengolang/prelude/template" + ws "github.com/rog-golang-buddies/rapidmidiex/api/websocket" rmx "github.com/rog-golang-buddies/rapidmidiex/internal" "github.com/rog-golang-buddies/rapidmidiex/internal/suid" - ws "github.com/rog-golang-buddies/rapidmidiex/www/websocket" ) type ( diff --git a/www/service.go b/api/service.go similarity index 91% rename from www/service.go rename to api/service.go index 7e2467ba..98963da9 100644 --- a/www/service.go +++ b/api/service.go @@ -1,4 +1,4 @@ -package www +package api import ( "log" @@ -8,8 +8,8 @@ import ( h "github.com/hyphengolang/prelude/http" + ws "github.com/rog-golang-buddies/rapidmidiex/api/websocket" "github.com/rog-golang-buddies/rapidmidiex/internal/suid" - ws "github.com/rog-golang-buddies/rapidmidiex/www/websocket" ) type Service struct { diff --git a/www/websocket/client.go b/api/websocket/client.go similarity index 100% rename from www/websocket/client.go rename to api/websocket/client.go diff --git a/www/websocket/conn.go b/api/websocket/conn.go similarity index 100% rename from www/websocket/conn.go rename to api/websocket/conn.go diff --git a/www/websocket/errors.go b/api/websocket/errors.go similarity index 100% rename from www/websocket/errors.go rename to api/websocket/errors.go diff --git a/www/websocket/pool.go b/api/websocket/pool.go similarity index 100% rename from www/websocket/pool.go rename to api/websocket/pool.go diff --git a/cmd/main.go b/cmd/main.go index 472718c3..150762e7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -15,7 +15,7 @@ import ( "github.com/spf13/viper" "golang.org/x/sync/errgroup" - "github.com/rog-golang-buddies/rapidmidiex/www" + "github.com/rog-golang-buddies/rapidmidiex/api" ) func main() { @@ -43,7 +43,7 @@ func run() error { srv := http.Server{ Addr: ":" + port, - Handler: cors.New(c).Handler(www.NewService(chi.NewMux())), + Handler: cors.New(c).Handler(api.NewService(chi.NewMux())), ReadTimeout: 10 * time.Second, // max time to read request from the client WriteTimeout: 10 * time.Second, // max time to write response to the client IdleTimeout: 120 * time.Second, // max time for connections using TCP Keep-Alive diff --git a/internal/tmp/jam_tmp.go b/internal/tmp/jam_tmp.go index 7b750461..476258f1 100644 --- a/internal/tmp/jam_tmp.go +++ b/internal/tmp/jam_tmp.go @@ -1,4 +1,4 @@ -// * In the process of taking useful code from here and moving to the `www` package +// * In the process of taking useful code from here and moving to the `api` package package tmp import ( @@ -78,7 +78,7 @@ func (s *jamSession) addConn(jc *jamConn) { // } // } -// * I have adapted this slightly inside the `www/ws` pacakge +// * I have adapted this slightly inside the `api/ws` package // iterates through session connections // and send provided message to each of them func (s *jamSession) broadcast(i interface{}) { @@ -93,9 +93,9 @@ func (s *jamSession) broadcast(i interface{}) { } } -// * Imo, a service should have a `ServeHTTP` method attatched if it is going to be talking -// * directly to the web, I have adapted this inside `www` pacakge -// * I have also decoupled the RESTful logic with sessions into a seperate data structure +// * Imo, a service should have a `ServeHTTP` method attached if it is going to be talking +// * directly to the web, I have adapted this inside `api` package +// * I have also decoupled the RESTful logic with sessions into a separate data structure // struct type for the Jam service // also contains current available sessions created by users type JamService struct { @@ -104,7 +104,7 @@ type JamService struct { } // * Better to define some of these inside the handlers themselves -// * An example of such pattern can be found inside the `www` pacakge +// * An example of such pattern can be found inside the `api` package // request types type ( newJamReq struct { diff --git a/internal/tmp/utils.go b/internal/tmp/utils.go index 8b586105..79ea843c 100644 --- a/internal/tmp/utils.go +++ b/internal/tmp/utils.go @@ -1,6 +1,6 @@ // * This file is not being used but saving as I have questions regarding the `isEmptyString` function -// * Parse not required as is the case with the errorResponse as those should really live in the `www` package -// * As there are function that depend on these utlities, I have refrained from deleting +// * Parse not required as is the case with the errorResponse as those should really live in the `api` package +// * As there are function that depend on these utilities, I have refrained from deleting package tmp import ( diff --git a/www/errors.go b/www/errors.go deleted file mode 100644 index 4f2a884d..00000000 --- a/www/errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package www - -import ( - "errors" -) - -var ( - ErrNoCookie = errors.New("www: cookie not found") - ErrSessionNotFound = errors.New("www: session not found") - ErrSessionExists = errors.New("www: session already exists") -) From 5970468a4fe9f18ae583fda84badbb5421d30290 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Tue, 20 Sep 2022 02:17:53 +0430 Subject: [PATCH 049/246] added POST to allowed methods --- api/service.go | 8 ++++---- cmd/main.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/service.go b/api/service.go index 98963da9..27d7fc35 100644 --- a/api/service.go +++ b/api/service.go @@ -19,7 +19,7 @@ type Service struct { c *ws.Client } -func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } func NewService(r chi.Router) *Service { s := &Service{r, log.Default(), ws.DefaultClient} @@ -27,14 +27,14 @@ func NewService(r chi.Router) *Service { return s } -func (s Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { +func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { h.Respond(w, r, data, status) } -func (s Service) fileServer(prefix string, dirname string) http.Handler { +func (s *Service) fileServer(prefix string, dirname string) http.Handler { return h.FileServer(prefix, dirname) } -func (S Service) parseUUID(w http.ResponseWriter, r *http.Request, key string) (suid.UUID, error) { +func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request, key string) (suid.UUID, error) { return suid.ParseString(chi.URLParam(r, key)) } diff --git a/cmd/main.go b/cmd/main.go index 150762e7..51c94178 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -37,7 +37,7 @@ func run() error { c := cors.Options{ AllowedOrigins: []string{"http://localhost:" + port, "http://localhost:5173"}, // ? band-aid, needs to change to a flag AllowCredentials: true, - AllowedMethods: []string{http.MethodGet}, + AllowedMethods: []string{http.MethodGet, http.MethodPost}, AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, } From 73046bb7d91f46e6db7a6b5d619f38c4eaea801b Mon Sep 17 00:00:00 2001 From: pmoieni Date: Tue, 20 Sep 2022 11:54:22 +0430 Subject: [PATCH 050/246] removed key from parseUUID and fixed a typo --- api/middleware.go | 2 +- api/routes.go | 22 +++++++++++----------- api/service.go | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/api/middleware.go b/api/middleware.go index 8962300a..c3cd3e9a 100644 --- a/api/middleware.go +++ b/api/middleware.go @@ -11,7 +11,7 @@ import ( func (s Service) sessionPool(f http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r, "id") + uid, err := s.parseUUID(w, r) if err != nil { s.respond(w, r, err, http.StatusBadRequest) return diff --git a/api/routes.go b/api/routes.go index b6c142f5..a8ca8c70 100644 --- a/api/routes.go +++ b/api/routes.go @@ -43,27 +43,27 @@ func (s *Service) routes() { func (s *Service) handleJamSession() http.HandlerFunc { type response struct { - MessageTyp rmx.MessageType `json:"type"` - ID suid.SUID `json:"id"` - SessionID suid.SUID `json:"sessionId"` + MessageType rmx.MessageType `json:"type"` + ID suid.SUID `json:"id"` + SessionID suid.SUID `json:"sessionId"` } return func(w http.ResponseWriter, r *http.Request) { c := r.Context().Value(upgradeKey).(*ws.Conn) defer func() { c.SendMessage(response{ - MessageTyp: rmx.Leave, - ID: c.ID.ShortUUID(), - SessionID: c.Pool().ID.ShortUUID(), + MessageType: rmx.Leave, + ID: c.ID.ShortUUID(), + SessionID: c.Pool().ID.ShortUUID(), }) c.Close() }() err := c.SendMessage(response{ - MessageTyp: rmx.Join, - ID: c.ID.ShortUUID(), - SessionID: c.Pool().ID.ShortUUID(), + MessageType: rmx.Join, + ID: c.ID.ShortUUID(), + SessionID: c.Pool().ID.ShortUUID(), }) if err != nil { @@ -110,7 +110,7 @@ func (s *Service) createSession() http.HandlerFunc { } func (s *Service) getSessionData(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r, "id") + uid, err := s.parseUUID(w, r) if err != nil { s.respond(w, r, err, http.StatusBadRequest) return @@ -175,7 +175,7 @@ func (s *Service) jamSessionHTML(path string) http.HandlerFunc { // ! I should be rendering a 404 page if there is an error // ! in this layer, but for an MVC this will do return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r, "id") + uid, err := s.parseUUID(w, r) if err != nil { s.respond(w, r, err, http.StatusBadRequest) return diff --git a/api/service.go b/api/service.go index 27d7fc35..dc5e72e0 100644 --- a/api/service.go +++ b/api/service.go @@ -35,6 +35,6 @@ func (s *Service) fileServer(prefix string, dirname string) http.Handler { return h.FileServer(prefix, dirname) } -func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request, key string) (suid.UUID, error) { - return suid.ParseString(chi.URLParam(r, key)) +func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { + return suid.ParseString(chi.URLParam(r, "id")) } From d411f17426f0b584b674a2ad6584a85038498a00 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Tue, 20 Sep 2022 14:31:59 -0400 Subject: [PATCH 051/246] Initial TUI client setup --- go.mod | 1 + go.sum | 6 +- ui/terminal/main.go | 108 +--------------------------- ui/terminal/tui/contants.go | 6 ++ ui/terminal/tui/lobbyui/lobby.go | 110 ++++++++++++++++++++++++++++ ui/terminal/tui/tui.go | 119 +++++++++++++++++++++++++++++++ 6 files changed, 243 insertions(+), 107 deletions(-) create mode 100644 ui/terminal/tui/contants.go create mode 100644 ui/terminal/tui/lobbyui/lobby.go create mode 100644 ui/terminal/tui/tui.go diff --git a/go.mod b/go.mod index c63c07af..fc87ec76 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( github.com/charmbracelet/bubbletea v0.22.1 + github.com/charmbracelet/lipgloss v0.6.0 github.com/go-chi/chi v1.5.4 github.com/go-chi/chi/v5 v5.0.7 github.com/gobwas/ws v1.1.0 diff --git a/go.sum b/go.sum index be6d59c0..c0e9124b 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/charmbracelet/bubbletea v0.22.1 h1:z66q0LWdJNOWEH9zadiAIXp2GN1AWrwNXU8obVY9X24= github.com/charmbracelet/bubbletea v0.22.1/go.mod h1:8/7hVvbPN6ZZPkczLiB8YpLkLJ0n7DMho5Wvfd2X1C0= +github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY= +github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -163,6 +165,7 @@ github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peK github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -172,9 +175,10 @@ github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTd github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= +github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= diff --git a/ui/terminal/main.go b/ui/terminal/main.go index 155ceaa8..8def5a0d 100644 --- a/ui/terminal/main.go +++ b/ui/terminal/main.go @@ -1,111 +1,7 @@ package main -import ( - "fmt" - "os" - - tea "github.com/charmbracelet/bubbletea" -) - -type model struct { - choices []string // items on the to-do list - cursor int // which to-do list item our cursor is pointing at - selected map[int]struct{} // which to-do items are selected -} - -func initialModel() model { - return model{ - // Our to-do list is a grocery list - choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"}, - - // A map which indicates which choices are selected. We're using - // the map like a mathematical set. The keys refer to the indexes - // of the `choices` slice, above. - selected: make(map[int]struct{}), - } -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - - // Is it a key press? - case tea.KeyMsg: - - // Cool, what was the actual key pressed? - switch msg.String() { - - // These keys should exit the program. - case "ctrl+c", "q": - return m, tea.Quit - - // The "up" and "k" keys move the cursor up - case "up", "k": - if m.cursor > 0 { - m.cursor-- - } - - // The "down" and "j" keys move the cursor down - case "down", "j": - if m.cursor < len(m.choices)-1 { - m.cursor++ - } - - // The "enter" key and the spacebar (a literal space) toggle - // the selected state for the item that the cursor is pointing at. - case "enter", " ": - _, ok := m.selected[m.cursor] - if ok { - delete(m.selected, m.cursor) - } else { - m.selected[m.cursor] = struct{}{} - } - } - } - - // Return the updated model to the Bubble Tea runtime for processing. - // Note that we're not returning a command. - return m, nil -} - -func (m model) View() string { - // The header - s := "What should we buy at the market?\n\n" - - // Iterate over our choices - for i, choice := range m.choices { - - // Is the cursor pointing at this choice? - cursor := " " // no cursor - if m.cursor == i { - cursor = ">" // cursor! - } - - // Is this choice selected? - checked := " " // not selected - if _, ok := m.selected[i]; ok { - checked = "x" // selected! - } - - // Render the row - s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice) - } - - // The footer - s += "\nPress q to quit.\n" - - // Send the UI for rendering - return s -} - -func (m model) Init() tea.Cmd { - // Just return `nil`, which means "no I/O right now, please." - return nil -} +import "github.com/rog-golang-buddies/rapidmidiex/ui/terminal/tui" func main() { - p := tea.NewProgram(initialModel()) - if err := p.Start(); err != nil { - fmt.Printf("Alas, there's been an error: %v", err) - os.Exit(1) - } + tui.Run() } diff --git a/ui/terminal/tui/contants.go b/ui/terminal/tui/contants.go new file mode 100644 index 00000000..e1ce0973 --- /dev/null +++ b/ui/terminal/tui/contants.go @@ -0,0 +1,6 @@ +package tui + +import "github.com/charmbracelet/lipgloss" + +// DocStyle styling for viewports +var DocStyle = lipgloss.NewStyle().Margin(0, 2) diff --git a/ui/terminal/tui/lobbyui/lobby.go b/ui/terminal/tui/lobbyui/lobby.go new file mode 100644 index 00000000..de2afa2a --- /dev/null +++ b/ui/terminal/tui/lobbyui/lobby.go @@ -0,0 +1,110 @@ +package lobbyui + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + tea "github.com/charmbracelet/bubbletea" +) + +// Message types +type statusMsg int + +type errMsg struct{ err error } + +type sessionsResp struct { + Sessions []Session `json:"sessions"` +} + +// For messages that contain errors it's often handy to also implement the +// error interface on the message. +func (e errMsg) Error() string { return e.err.Error() } + +// Commands +func FetchSessions(baseURL string) tea.Cmd { + fmt.Println(baseURL) + return func() tea.Msg { + // Create an HTTP client and make a GET request. + c := &http.Client{Timeout: 10 * time.Second} + res, err := c.Get(baseURL + "/api/v1/jam") + if err != nil { + fmt.Println(err) + // There was an error making our request. Wrap the error we received + // in a message and return it. + return errMsg{err} + } + // We received a response from the server. Return the HTTP status code + // as a message. + decoder := json.NewDecoder(res.Body) + var resp sessionsResp + decoder.Decode(&resp) + + return resp + } +} + +type Session struct { + Id string `json:"sessionId"` // TODO: Need to fix the API to return "id" + // UserCount int `json:"userCount"` +} + +type Model struct { + sessions []Session + status int + err error +} + +// Init needed to satisfy Model interface. It doesn't seem to be called on sub-models. +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + switch msg := msg.(type) { + case statusMsg: + // The server returned a status message. Save it to our model. Also + // tell the Bubble Tea runtime we want to exit because we have nothing + // else to do. We'll still be able to render a final view with our + // status message. + m.status = int(msg) + cmds = append(cmds, tea.Quit) + case sessionsResp: + m.sessions = msg.Sessions + cmds = append(cmds, tea.Quit) + fmt.Println(m.sessions) + case errMsg: + // There was an error. Note it in the model. And tell the runtime + // we're done and want to quit. + m.err = msg + cmds = append(cmds, tea.Quit) + } + return m, tea.Batch(cmds...) +} + +func (m Model) View() string { + // If there's an error, print it out and don't do anything else. + if m.err != nil { + return fmt.Sprintf("\nWe had some trouble: %v\n\n", m.err) + } + + // Tell the user we're doing something. + s := fmt.Sprintln("Fetching Jam Sessions...") + // When the server responds with a status, add it to the current line. + if m.status > 0 { + s += fmt.Sprintf("%d %s!", m.status, http.StatusText(m.status)) + } + + if m.sessions != nil && len(m.sessions) > 0 { + s += fmt.Sprintf("Available Sessions: %v", m.sessions) + } + + // Send off whatever we came up with above for rendering. + return "\n" + s + "\n\n" +} + +func New() tea.Model { + return Model{} +} diff --git a/ui/terminal/tui/tui.go b/ui/terminal/tui/tui.go new file mode 100644 index 00000000..10735636 --- /dev/null +++ b/ui/terminal/tui/tui.go @@ -0,0 +1,119 @@ +package tui + +import ( + "fmt" + "net/url" + "os" + + tea "github.com/charmbracelet/bubbletea" + "github.com/rog-golang-buddies/rapidmidiex/internal/suid" + "github.com/rog-golang-buddies/rapidmidiex/ui/terminal/tui/lobbyui" + lobby "github.com/rog-golang-buddies/rapidmidiex/ui/terminal/tui/lobbyui" +) + +// ******** +// Code heavily based on "Project Journal" +// https://github.com/bashbunni/pjs +// https://www.youtube.com/watch?v=uJ2egAkSkjg&t=319s +// ******** + +type Session struct { + Id suid.UUID `json:"id"` + // UserCount int `json:"userCount"` +} + +type appView int + +const ( + jamView appView = iota + lobbyView +) + +type mainModel struct { + curView appView + lobby tea.Model + jam tea.Model + RESTendpoint string + WSendpoint string +} + +func NewModel(serverHostURL string) (mainModel, error) { + wsURL, err := url.Parse(serverHostURL) + if err != nil { + return mainModel{}, err + } + wsURL.Scheme = "ws" + + return mainModel{ + curView: lobbyView, + lobby: lobby.New(), + RESTendpoint: serverHostURL + "/api/v1", + WSendpoint: wsURL.String() + "/ws", + }, nil +} + +func (m mainModel) Init() tea.Cmd { + return lobbyui.FetchSessions(m.RESTendpoint) +} + +func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + var cmds []tea.Cmd + // Handle incoming messages + switch msg := msg.(type) { + + case tea.KeyMsg: + // Ctrl+c exits. Even with short running programs it's good to have + // a quit key, just incase your logic is off. Users will be very + // annoyed if they can't exit. + if msg.Type == tea.KeyCtrlC { + return m, tea.Quit + } + } + + // Call sub-model Updates + switch m.curView { + case lobbyView: + newLobby, newCmd := m.lobby.Update(msg) + lobbyModel, ok := newLobby.(lobby.Model) + if !ok { + panic("could not perform assertion on lobbyui model") + } + m.lobby = lobbyModel + cmd = newCmd + case jamView: + } + + // Run all commands from sub-model Updates + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) + +} + +func (m mainModel) View() string { + switch m.curView { + // TODO: Add JamView + default: + return m.lobby.View() + } +} + +func Run() { + // TODO: Get from args, user input, or env + const serverHostURL = "http://localhost:9003" + m, err := NewModel(serverHostURL) + if err != nil { + bail(err) + } + + if err := tea.NewProgram(m).Start(); err != nil { + bail(err) + } +} + +func bail(err error) { + if err != nil { + fmt.Printf("Uh oh, there was an error: %v\n", err) + os.Exit(1) + } +} From e798346fe8b28c99ae7c96ae29ef498c6ef8827b Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Tue, 20 Sep 2022 15:09:07 -0400 Subject: [PATCH 052/246] Initial Jam view connected --- go.mod | 3 +- go.sum | 9 +++ ui/terminal/tui/contants.go | 6 -- ui/terminal/tui/jamui/jam.go | 119 +++++++++++++++++++++++++++++++ ui/terminal/tui/lobbyui/lobby.go | 79 +++++++++++++++----- ui/terminal/tui/tui.go | 18 +++-- 6 files changed, 206 insertions(+), 28 deletions(-) delete mode 100644 ui/terminal/tui/contants.go create mode 100644 ui/terminal/tui/jamui/jam.go diff --git a/go.mod b/go.mod index fc87ec76..0341f6b4 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/rog-golang-buddies/rapidmidiex go 1.18 require ( + github.com/charmbracelet/bubbles v0.14.0 github.com/charmbracelet/bubbletea v0.22.1 github.com/charmbracelet/lipgloss v0.6.0 github.com/go-chi/chi v1.5.4 @@ -14,6 +15,7 @@ require ( github.com/rs/cors v1.8.2 github.com/spf13/viper v1.12.0 golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 ) require ( @@ -27,7 +29,6 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.12.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect ) require ( diff --git a/go.sum b/go.sum index c0e9124b..d68f9b05 100644 --- a/go.sum +++ b/go.sum @@ -38,9 +38,15 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/charmbracelet/bubbles v0.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og= +github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= +github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4= github.com/charmbracelet/bubbletea v0.22.1 h1:z66q0LWdJNOWEH9zadiAIXp2GN1AWrwNXU8obVY9X24= github.com/charmbracelet/bubbletea v0.22.1/go.mod h1:8/7hVvbPN6ZZPkczLiB8YpLkLJ0n7DMho5Wvfd2X1C0= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY= github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -154,6 +160,7 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -173,6 +180,7 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= @@ -200,6 +208,7 @@ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUA github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= diff --git a/ui/terminal/tui/contants.go b/ui/terminal/tui/contants.go deleted file mode 100644 index e1ce0973..00000000 --- a/ui/terminal/tui/contants.go +++ /dev/null @@ -1,6 +0,0 @@ -package tui - -import "github.com/charmbracelet/lipgloss" - -// DocStyle styling for viewports -var DocStyle = lipgloss.NewStyle().Margin(0, 2) diff --git a/ui/terminal/tui/jamui/jam.go b/ui/terminal/tui/jamui/jam.go new file mode 100644 index 00000000..c921b9ea --- /dev/null +++ b/ui/terminal/tui/jamui/jam.go @@ -0,0 +1,119 @@ +package jamui + +import ( + "fmt" + "os" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "golang.org/x/term" +) + +const ( + // In real life situations we'd adjust the document to fit the width we've + // detected. In the case of this example we're hardcoding the width, and + // later using the detected width only to truncate in order to avoid jaggy + // wrapping. + width = 96 + + columnWidth = 30 +) + +// DocStyle styling for viewports +var ( + subtle = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"} + highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} + special = lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"} + + docStyle = lipgloss.NewStyle().Padding(1, 2, 1, 2) + + keyBorder = lipgloss.Border{ + Top: "─", + Bottom: "-", + Left: "│", + Right: "│", + TopLeft: "╭", + TopRight: "╮", + BottomLeft: "╰", + BottomRight: "╯", + } + + key = lipgloss.NewStyle(). + Align(lipgloss.Center). + Border(keyBorder, true). + BorderForeground(highlight). + Padding(0, 1) +) + +type pianoKey struct { + noteNumber int // MIDI note number ie: 72 + name string // Name of musical note, ie: "C5" + keyMap string // Mapped qwerty keyboard key. Ex: "q" +} + +type Model struct { + piano []pianoKey // Piano keys. {"q": pianoKey{72, "C5", "q", ...}} + activeKeys map[string]struct{} // Currently active piano keys +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + + // Is it a key press? + case tea.KeyMsg: + switch msg.String() { + // These keys should exit the program. + case "ctrl+c": + return m, tea.Quit + default: + fmt.Printf("Key press: %s\n", msg.String()) + } + + } + return m, nil +} + +func (m Model) View() string { + physicalWidth, _, _ := term.GetSize(int(os.Stdout.Fd())) + doc := strings.Builder{} + + if physicalWidth > 0 { + docStyle = docStyle.MaxWidth(physicalWidth) + } + + // Keyboard + keyboard := lipgloss.JoinHorizontal(lipgloss.Top, + key.Render("C5"+"\n\n"+"(q)"), + key.Render("D5"+"\n\n"+"(w)"), + key.Render("E5"+"\n\n"+"(e)"), + key.Render("F5"+"\n\n"+"(r)"), + key.Render("G5"+"\n\n"+"(t)"), + key.Render("A5"+"\n\n"+"(y)"), + key.Render("B5"+"\n\n"+"(u)"), + key.Render("C6"+"\n\n"+"(i)"), + ) + doc.WriteString(keyboard + "\n\n") + return docStyle.Render(doc.String()) +} + +func New() tea.Model { + return Model{ + piano: []pianoKey{ + {72, "C5", "q"}, + {74, "D5", "w"}, + {76, "E5", "e"}, + {77, "F5", "r"}, + {79, "G5", "t"}, + {81, "A5", "y"}, + {83, "B5", "u"}, + {84, "C6", "i"}, + }, + + activeKeys: make(map[string]struct{}), + } +} diff --git a/ui/terminal/tui/lobbyui/lobby.go b/ui/terminal/tui/lobbyui/lobby.go index de2afa2a..72910dfa 100644 --- a/ui/terminal/tui/lobbyui/lobby.go +++ b/ui/terminal/tui/lobbyui/lobby.go @@ -6,15 +6,26 @@ import ( "net/http" "time" + "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) +// Styles +var baseStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")) + // Message types type statusMsg int type errMsg struct{ err error } -type sessionsResp struct { +type Session struct { + Id string `json:"sessionId"` // TODO: Need to fix the API to return "id" + // UserCount int `json:"userCount"` +} +type jamsResp struct { Sessions []Session `json:"sessions"` } @@ -24,34 +35,31 @@ func (e errMsg) Error() string { return e.err.Error() } // Commands func FetchSessions(baseURL string) tea.Cmd { - fmt.Println(baseURL) return func() tea.Msg { // Create an HTTP client and make a GET request. c := &http.Client{Timeout: 10 * time.Second} - res, err := c.Get(baseURL + "/api/v1/jam") + res, err := c.Get(baseURL + "/jam") if err != nil { - fmt.Println(err) // There was an error making our request. Wrap the error we received // in a message and return it. return errMsg{err} } - // We received a response from the server. Return the HTTP status code + // We received a response from the server. + // Return the HTTP status code // as a message. + if res.StatusCode >= 400 { + return statusMsg(res.StatusCode) + } decoder := json.NewDecoder(res.Body) - var resp sessionsResp + var resp jamsResp decoder.Decode(&resp) - return resp } } -type Session struct { - Id string `json:"sessionId"` // TODO: Need to fix the API to return "id" - // UserCount int `json:"userCount"` -} - type Model struct { sessions []Session + jamTable table.Model status int err error } @@ -71,10 +79,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // status message. m.status = int(msg) cmds = append(cmds, tea.Quit) - case sessionsResp: + case jamsResp: m.sessions = msg.Sessions + m.jamTable = makeJamsTable(m) + cmds = append(cmds, tea.Quit) - fmt.Println(m.sessions) case errMsg: // There was an error. Note it in the model. And tell the runtime // we're done and want to quit. @@ -97,8 +106,8 @@ func (m Model) View() string { s += fmt.Sprintf("%d %s!", m.status, http.StatusText(m.status)) } - if m.sessions != nil && len(m.sessions) > 0 { - s += fmt.Sprintf("Available Sessions: %v", m.sessions) + if m.sessions != nil { + s += baseStyle.Render(m.jamTable.View()) + "\n" } // Send off whatever we came up with above for rendering. @@ -108,3 +117,41 @@ func (m Model) View() string { func New() tea.Model { return Model{} } + +// https://github.com/rog-golang-buddies/rapidmidiex-research/issues/9#issuecomment-1204853876 +func makeJamsTable(m Model) table.Model { + columns := []table.Column{ + {Title: "Name", Width: 15}, + {Title: "ID", Width: 15}, + {Title: "Players", Width: 10}, + // {Title: "Latency", Width: 4}, + } + + rows := make([]table.Row, 0) + + for _, s := range m.sessions { + row := table.Row{"Name Here", s.Id, "0"} + rows = append(rows, row) + } + + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(7), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(false) + t.SetStyles(s) + + return t +} diff --git a/ui/terminal/tui/tui.go b/ui/terminal/tui/tui.go index 10735636..c8a895e4 100644 --- a/ui/terminal/tui/tui.go +++ b/ui/terminal/tui/tui.go @@ -7,8 +7,8 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/rog-golang-buddies/rapidmidiex/internal/suid" + "github.com/rog-golang-buddies/rapidmidiex/ui/terminal/tui/jamui" "github.com/rog-golang-buddies/rapidmidiex/ui/terminal/tui/lobbyui" - lobby "github.com/rog-golang-buddies/rapidmidiex/ui/terminal/tui/lobbyui" ) // ******** @@ -46,7 +46,8 @@ func NewModel(serverHostURL string) (mainModel, error) { return mainModel{ curView: lobbyView, - lobby: lobby.New(), + lobby: lobbyui.New(), + jam: jamui.New(), RESTendpoint: serverHostURL + "/api/v1", WSendpoint: wsURL.String() + "/ws", }, nil @@ -75,15 +76,21 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch m.curView { case lobbyView: newLobby, newCmd := m.lobby.Update(msg) - lobbyModel, ok := newLobby.(lobby.Model) + lobbyModel, ok := newLobby.(lobbyui.Model) if !ok { panic("could not perform assertion on lobbyui model") } m.lobby = lobbyModel cmd = newCmd case jamView: + newJam, newCmd := m.jam.Update(msg) + jamModel, ok := newJam.(jamui.Model) + if !ok { + panic("could not perform assertion on jamui model") + } + m.jam = jamModel + cmd = newCmd } - // Run all commands from sub-model Updates cmds = append(cmds, cmd) return m, tea.Batch(cmds...) @@ -92,7 +99,8 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m mainModel) View() string { switch m.curView { - // TODO: Add JamView + case jamView: + return m.jam.View() default: return m.lobby.View() } From f148dfac458c8a50693b3f6ac4af6616217f858f Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Tue, 20 Sep 2022 18:22:18 -0400 Subject: [PATCH 053/246] Join Jam --- api/routes.go | 10 +++++--- ui/terminal/tui/lobbyui/lobby.go | 43 ++++++++++++++++++++++++++++---- ui/terminal/tui/tui.go | 10 +++++--- 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/api/routes.go b/api/routes.go index a8ca8c70..7db302c4 100644 --- a/api/routes.go +++ b/api/routes.go @@ -15,8 +15,9 @@ import ( type ( session struct { Name string `json:"name,omitempty"` - SessionID suid.SUID `json:"sessionId,omitempty"` + ID suid.SUID `json:"id,omitempty"` Users []suid.SUID `json:"users,omitempty"` + UserCount int `json:"userCount"` } ) @@ -124,8 +125,8 @@ func (s *Service) getSessionData(w http.ResponseWriter, r *http.Request) { } v := &session{ - SessionID: p.ID.ShortUUID(), - Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), + ID: p.ID.ShortUUID(), + Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), } s.respond(w, r, v, http.StatusOK) @@ -143,7 +144,8 @@ func (s *Service) listSessions() http.HandlerFunc { for _, p := range pl { sl = append(sl, session{ Name: "", // name not implemented yet - SessionID: p.ID.ShortUUID(), + ID: p.ID.ShortUUID(), + UserCount: p.Size(), }) } diff --git a/ui/terminal/tui/lobbyui/lobby.go b/ui/terminal/tui/lobbyui/lobby.go index 72910dfa..8b1e76fb 100644 --- a/ui/terminal/tui/lobbyui/lobby.go +++ b/ui/terminal/tui/lobbyui/lobby.go @@ -9,6 +9,7 @@ import ( "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/gorilla/websocket" ) // Styles @@ -22,7 +23,8 @@ type statusMsg int type errMsg struct{ err error } type Session struct { - Id string `json:"sessionId"` // TODO: Need to fix the API to return "id" + Id string `json:"id"` // TODO: Need to fix the API to return "id" + Name string `json:"name"` // UserCount int `json:"userCount"` } type jamsResp struct { @@ -58,6 +60,7 @@ func FetchSessions(baseURL string) tea.Cmd { } type Model struct { + wsURL string sessions []Session jamTable table.Model status int @@ -82,14 +85,23 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case jamsResp: m.sessions = msg.Sessions m.jamTable = makeJamsTable(m) - - cmds = append(cmds, tea.Quit) + m.jamTable.Focus() case errMsg: // There was an error. Note it in the model. And tell the runtime // we're done and want to quit. m.err = msg cmds = append(cmds, tea.Quit) + case tea.KeyMsg: + switch msg.String() { + case tea.KeyEnter.String(): + jamId := m.jamTable.SelectedRow()[1] + + cmds = append(cmds, jamConnect(m.wsURL, jamId)) + } } + newJamTable, cmd := m.jamTable.Update(msg) + m.jamTable = newJamTable + cmds = append(cmds, cmd) return m, tea.Batch(cmds...) } @@ -114,8 +126,10 @@ func (m Model) View() string { return "\n" + s + "\n\n" } -func New() tea.Model { - return Model{} +func New(wsURL string) tea.Model { + return Model{ + wsURL: wsURL, + } } // https://github.com/rog-golang-buddies/rapidmidiex-research/issues/9#issuecomment-1204853876 @@ -155,3 +169,22 @@ func makeJamsTable(m Model) table.Model { return t } + +type JamConnected struct { + WS *websocket.Conn +} + +func jamConnect(baseURL, jamID string) tea.Cmd { + return func() tea.Msg { + url := baseURL + "/" + jamID + fmt.Println("ws url", url) + ws, _, err := websocket.DefaultDialer.Dial(url, nil) + if err != nil { + return errMsg{fmt.Errorf("jamConnect: %v\n%v", url, err)} + } + // TODO: Actually connect to Jam Session over websocket + return JamConnected{ + WS: ws, + } + } +} diff --git a/ui/terminal/tui/tui.go b/ui/terminal/tui/tui.go index c8a895e4..20496027 100644 --- a/ui/terminal/tui/tui.go +++ b/ui/terminal/tui/tui.go @@ -6,6 +6,7 @@ import ( "os" tea "github.com/charmbracelet/bubbletea" + "github.com/gorilla/websocket" "github.com/rog-golang-buddies/rapidmidiex/internal/suid" "github.com/rog-golang-buddies/rapidmidiex/ui/terminal/tui/jamui" "github.com/rog-golang-buddies/rapidmidiex/ui/terminal/tui/lobbyui" @@ -35,6 +36,7 @@ type mainModel struct { jam tea.Model RESTendpoint string WSendpoint string + jamSocket *websocket.Conn // Websocket connection to a Jam Session } func NewModel(serverHostURL string) (mainModel, error) { @@ -46,10 +48,9 @@ func NewModel(serverHostURL string) (mainModel, error) { return mainModel{ curView: lobbyView, - lobby: lobbyui.New(), + lobby: lobbyui.New(wsURL.String() + "/ws"), jam: jamui.New(), RESTendpoint: serverHostURL + "/api/v1", - WSendpoint: wsURL.String() + "/ws", }, nil } @@ -60,7 +61,7 @@ func (m mainModel) Init() tea.Cmd { func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd - // Handle incoming messages + // Handle incoming messages from I/O switch msg := msg.(type) { case tea.KeyMsg: @@ -70,6 +71,9 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Type == tea.KeyCtrlC { return m, tea.Quit } + case lobbyui.JamConnected: + m.curView = jamView + m.jamSocket = msg.WS } // Call sub-model Updates From 0f864c5a2eca7b383b3222a841413f7f644d3541 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Wed, 21 Sep 2022 03:59:39 -0400 Subject: [PATCH 054/246] fix ws route --- api/routes.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api/routes.go b/api/routes.go index 7db302c4..9ab01f1e 100644 --- a/api/routes.go +++ b/api/routes.go @@ -29,9 +29,6 @@ func (s *Service) routes() { s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) s.r.Get("/", s.indexHTML("ui/www/index.html")) s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) - // s.r.Get("/api/jam/create", s.createSession()) - // s.r.Get("/api/jam/{id}", s.getSessionData()) - // s.r.HandleFunc("/jam/{id}", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) // REST v1 s.r.Get("/api/v1/jam", s.listSessions()) @@ -39,7 +36,7 @@ func (s *Service) routes() { s.r.Get("/api/v1/jam/{id}", s.getSessionData) // Websocket - s.r.Get("/ws", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) + s.r.Get("/ws/{id}", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) } func (s *Service) handleJamSession() http.HandlerFunc { From af2cc318a314fb9ef96517b01a157db30a6a13fb Mon Sep 17 00:00:00 2001 From: adoublef Date: Wed, 21 Sep 2022 18:37:33 +0100 Subject: [PATCH 055/246] api changes --- assets/src/play.js | 2 +- cmd/main.go | 14 ++--- www/middleware.go | 47 ++++++++-------- www/routes.go | 128 ++++++++++++------------------------------ www/service.go | 25 ++++++++- www/websocket/conn.go | 14 ++++- www/www.go | 12 +++- 7 files changed, 114 insertions(+), 128 deletions(-) diff --git a/assets/src/play.js b/assets/src/play.js index b0558602..9de0c81c 100644 --- a/assets/src/play.js +++ b/assets/src/play.js @@ -9,7 +9,7 @@ app.innerHTML = `
              `; -const ws = new WebSocket(websocketUrl(`/ws`)); +const ws = new WebSocket(websocketUrl(`/api/v1/jam/${sessionId()}/ws`)); ws.addEventListener('open', (e) => { document.querySelector('button').disabled = false; diff --git a/cmd/main.go b/cmd/main.go index 55fa7cd0..82283c46 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -25,16 +25,16 @@ func main() { func run() error { // ? want to move to viper ASAP - port := getEnv("PORT", "8888") + port := getEnv("PORT", "8889") sCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) defer cancel() // ? should this defined within the instantiation of a new service c := cors.Options{ - AllowedOrigins: []string{"http://localhost:5173"}, // ? band-aid, needs to change to a flag + AllowedOrigins: []string{"*"}, // ? band-aid, needs to change to a flag AllowCredentials: true, - AllowedMethods: []string{http.MethodGet}, + AllowedMethods: []string{http.MethodGet, http.MethodPost}, AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, } @@ -76,14 +76,14 @@ func getEnv(key, fallback string) string { func init() { // // name of config file (without extension) - // viper.SetConfigName("config") + // viper.SetConfigName("config") // // REQUIRED if the config file does not have the extension in the name - // viper.SetConfigType("env") + // viper.SetConfigType("env") // // optionally look for config in the working directory - // viper.AddConfigPath(".") + // viper.AddConfigPath(".") //// Set Default variables - // viper.SetDefault("PORT", "8080") + // viper.SetDefault("PORT", "8080") // viper.AutomaticEnv() diff --git a/www/middleware.go b/www/middleware.go index e968c3e6..b49aa771 100644 --- a/www/middleware.go +++ b/www/middleware.go @@ -9,9 +9,9 @@ import ( ws "github.com/rog-golang-buddies/rapidmidiex/www/websocket" ) -func (s Service) sessionPool(f http.HandlerFunc) http.HandlerFunc { +func (s Service) wsConnectionPool(f http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r, "id") + uid, err := s.parseUUID(w, r) if err != nil { s.respond(w, r, err, http.StatusBadRequest) return @@ -19,7 +19,7 @@ func (s Service) sessionPool(f http.HandlerFunc) http.HandlerFunc { p, err := s.c.Get(uid) if err != nil { - s.l.Println(err) + s.respond(w, r, err, http.StatusNotFound) return } @@ -28,28 +28,29 @@ func (s Service) sessionPool(f http.HandlerFunc) http.HandlerFunc { } } -func (s Service) upgradeHTTP(f http.HandlerFunc) http.HandlerFunc { - u := &websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - CheckOrigin: func(r *http.Request) bool { return true }, - } - - return func(w http.ResponseWriter, r *http.Request) { - p := r.Context().Value(roomKey).(*ws.Pool) - - if p.Size() == p.MaxConn { - s.respond(w, r, ws.ErrMaxConn, http.StatusUnauthorized) - return +func (s Service) upgradeHTTP(readBuf, writeBuf int) func(f http.HandlerFunc) http.HandlerFunc { + return func(f http.HandlerFunc) http.HandlerFunc { + u := &websocket.Upgrader{ + ReadBufferSize: readBuf, + WriteBufferSize: writeBuf, + CheckOrigin: func(r *http.Request) bool { return true }, } - c, err := p.NewConn(w, r, u) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return + return func(w http.ResponseWriter, r *http.Request) { + p := r.Context().Value(roomKey).(*ws.Pool) + if p.Size() == p.MaxConn { + s.respond(w, r, ws.ErrMaxConn, http.StatusUnauthorized) + return + } + + c, err := p.NewConn(w, r, u) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + r = r.WithContext(context.WithValue(r.Context(), upgradeKey, c)) + f(w, r) } - - r = r.WithContext(context.WithValue(r.Context(), upgradeKey, c)) - f(w, r) } } diff --git a/www/routes.go b/www/routes.go index b1a3ffc6..9e3c603a 100644 --- a/www/routes.go +++ b/www/routes.go @@ -1,10 +1,9 @@ package www import ( + "encoding/json" "net/http" - "github.com/go-chi/chi/v5/middleware" - t "github.com/hyphengolang/prelude/template" rmx "github.com/rog-golang-buddies/rapidmidiex/internal" @@ -12,55 +11,39 @@ import ( ws "github.com/rog-golang-buddies/rapidmidiex/www/websocket" ) -func (s *Service) routes() { - // middleware - s.r.Use(middleware.Logger) - - // temporary static files - // s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) - // s.r.Get("/", s.indexHTML("ui/www/index.html")) - // s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) - - // v0 - // s.r.Get("/api/jam/create", s.createSession()) - // s.r.Get("/api/jam/{id}", s.getSessionData()) - // s.r.HandleFunc("/jam/{id}", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) - - // REST v1 - s.r.Get("/api/v1/jam", s.listSessions()) - s.r.Post("/api/v1/jam", s.createSession()) - s.r.Get("/api/v1/jam/{id}", s.getSessionData) +func (s *Service) handleP2PComms() http.HandlerFunc { + type response[T any] struct { + MessageTyp rmx.MessageType `json:"type"` + Data T `json:"data"` + } - // Websocket - s.r.Get("/ws", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) -} + type join struct { + ID suid.SUID `json:"id"` + SessionID suid.SUID `json:"sessionId"` + } -func (s *Service) handleJamSession() http.HandlerFunc { - type response struct { - MessageTyp rmx.MessageType `json:"type"` - ID suid.SUID `json:"id"` - SessionID suid.SUID `json:"sessionId"` + type leave struct { + ID suid.SUID `json:"id"` + SessionID suid.SUID `json:"sessionId"` + Error any `json:"err"` } return func(w http.ResponseWriter, r *http.Request) { c := r.Context().Value(upgradeKey).(*ws.Conn) defer func() { - c.SendMessage(response{ + // ! send error when Leaving session pool + c.SendMessage(response[leave]{ MessageTyp: rmx.Leave, - ID: c.ID.ShortUUID(), - SessionID: c.Pool().ID.ShortUUID(), + Data: leave{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, }) c.Close() }() - err := c.SendMessage(response{ + if err := c.SendMessage(response[join]{ MessageTyp: rmx.Join, - ID: c.ID.ShortUUID(), - SessionID: c.Pool().ID.ShortUUID(), - }) - - if err != nil { + Data: join{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, + }); err != nil { s.l.Println(err) return } @@ -69,13 +52,15 @@ func (s *Service) handleJamSession() http.HandlerFunc { // ? this for-loop only needs to read and // ? never touch the code for writing for { - var n int - if err := c.ReadJSON(&n); err != nil { + var msg response[json.RawMessage] + if err := c.ReadJSON(&msg); err != nil { s.l.Println(err) return } - if err := c.SendMessage(n + 10); err != nil { + // * here the message will be passed off to a different handler + // * via a go routine* + if err := c.SendMessage(response[int]{MessageTyp: rmx.Message, Data: 10}); err != nil { s.l.Println(err) return } @@ -83,11 +68,7 @@ func (s *Service) handleJamSession() http.HandlerFunc { } } -func (s *Service) createSession() http.HandlerFunc { - // type response struct { - // SessionID suid.SUID `json:"sessionId"` - // } - +func (s *Service) handleCreateRoom() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, err := s.c.NewPool() if err != nil { @@ -95,23 +76,15 @@ func (s *Service) createSession() http.HandlerFunc { return } - v := Session{ - SessionID: suid.FromUUID(uid), - } + v := Session{ID: suid.FromUUID(uid)} s.respond(w, r, v, http.StatusOK) } } -<<<<<<< HEAD -func (s *Service) getSessionData() http.HandlerFunc { - // type response struct { - // SessionID suid.SUID `json:"sessionId"` - // Users []suid.SUID `json:"users"` - // } - +func (s *Service) handleGetRoom() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r, "id") + uid, err := s.parseUUID(w, r) if err != nil { s.respond(w, r, err, http.StatusBadRequest) return @@ -125,57 +98,30 @@ func (s *Service) getSessionData() http.HandlerFunc { } v := &Session{ - SessionID: p.ID.ShortUUID(), - Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), + ID: p.ID.ShortUUID(), + Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), } -======= -func (s *Service) getSessionData(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r, "id") - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - // ! rename method as `Get` is nondescriptive - p, err := s.c.Get(uid) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } ->>>>>>> e30b8982b1bdb666dcb0b6858447e305d281de24 - v := &session{ - SessionID: p.ID.ShortUUID(), - Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), + s.respond(w, r, v, http.StatusOK) } - - s.respond(w, r, v, http.StatusOK) } -func (s *Service) listSessions() http.HandlerFunc { +func (s *Service) handleListRooms() http.HandlerFunc { type response struct { Sessions []Session `json:"sessions"` } return func(w http.ResponseWriter, r *http.Request) { - // pl := s.c.List() - - // sl := make([]session, 0, len(pl)) - // for _, p := range pl { - // sl = append(sl, session{ - // Name: "", // name not implemented yet - // SessionID: p.ID.ShortUUID(), - // }) - // } - v := &response{ - Sessions: rmx.FMap(s.c.List(), func(p *ws.Pool) Session { return Session{SessionID: p.ID.ShortUUID()} }), + Sessions: rmx.FMap(s.c.List(), func(p *ws.Pool) Session { return Session{ID: p.ID.ShortUUID()} }), } s.respond(w, r, v, http.StatusOK) } } +// ! to be discarded + func (s *Service) indexHTML(path string) http.HandlerFunc { render, err := t.Render(path) if err != nil { @@ -196,7 +142,7 @@ func (s *Service) jamSessionHTML(path string) http.HandlerFunc { // ! I should be rendering a 404 page if there is an error // ! in this layer, but for an MVC this will do return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r, "id") + uid, err := s.parseUUID(w, r) if err != nil { s.respond(w, r, err, http.StatusBadRequest) return diff --git a/www/service.go b/www/service.go index 7e2467ba..d1173b90 100644 --- a/www/service.go +++ b/www/service.go @@ -4,6 +4,7 @@ import ( "log" "net/http" + "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" h "github.com/hyphengolang/prelude/http" @@ -31,10 +32,30 @@ func (s Service) respond(w http.ResponseWriter, r *http.Request, data any, statu h.Respond(w, r, data, status) } +func (s Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { + return h.Decode(w, r, data) +} + func (s Service) fileServer(prefix string, dirname string) http.Handler { return h.FileServer(prefix, dirname) } -func (S Service) parseUUID(w http.ResponseWriter, r *http.Request, key string) (suid.UUID, error) { - return suid.ParseString(chi.URLParam(r, key)) +func (S Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { + return suid.ParseString(chi.URLParam(r, "id")) +} + +func (s *Service) routes() { + // middleware + s.r.Use(middleware.Logger) + + // temporary static files + s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) + s.r.Get("/", s.indexHTML("ui/www/index.html")) + s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) + + // v1 + s.r.Get("/api/v1/jam", s.handleListRooms()) + s.r.Post("/api/v1/jam", s.handleCreateRoom()) + s.r.Get("/api/v1/jam/{id}", s.handleGetRoom()) + s.r.Get("/api/v1/jam/{id}/ws", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.wsConnectionPool)) } diff --git a/www/websocket/conn.go b/www/websocket/conn.go index 6f7fb92f..f9071b98 100644 --- a/www/websocket/conn.go +++ b/www/websocket/conn.go @@ -2,6 +2,7 @@ package websocket import ( "github.com/gorilla/websocket" + rmx "github.com/rog-golang-buddies/rapidmidiex/internal" "github.com/rog-golang-buddies/rapidmidiex/internal/suid" ) @@ -24,4 +25,15 @@ func (c Conn) ReadJSON(v any) error { return c.rwc.ReadJSON(v) } func (c Conn) WriteJSON(v any) error { return c.rwc.WriteJSON(v) } -func (c Conn) SendMessage(v any) error { c.p.msgs <- v; return nil } +func (c Conn) SendMessage(v any) error { + c.p.msgs <- v + return nil +} + +func (c Conn) SendMessage2(typ rmx.MessageType, data any) error { + v := struct { + Typ rmx.MessageType + }{} + c.p.msgs <- v + return nil +} diff --git a/www/www.go b/www/www.go index 8f6c105d..76945491 100644 --- a/www/www.go +++ b/www/www.go @@ -18,7 +18,13 @@ var ( func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } type Session struct { - Name string `json:"name,omitempty"` - SessionID suid.SUID `json:"sessionId,omitempty"` - Users []suid.SUID `json:"users,omitempty"` + ID suid.SUID `json:"id"` + Name string `json:"name,omitempty"` + Users []suid.SUID `json:"users,omitempty"` +} + +type User struct { + ID suid.SUID `json:"id"` + Name string `json:"name,omitempty"` + /* More fields can belong here */ } From 55857ae0dc442eeb4e2cd98c4abcef8256780b51 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Wed, 21 Sep 2022 13:41:17 -0400 Subject: [PATCH 056/246] Status and help menu (not working) --- ui/terminal/tui/lobbyui/help.go | 91 ++++++++++++++++++++++ ui/terminal/tui/lobbyui/lobby.go | 126 +++++++++++++++++++++++-------- ui/terminal/tui/tui.go | 3 +- 3 files changed, 188 insertions(+), 32 deletions(-) create mode 100644 ui/terminal/tui/lobbyui/help.go diff --git a/ui/terminal/tui/lobbyui/help.go b/ui/terminal/tui/lobbyui/help.go new file mode 100644 index 00000000..1b66ad8f --- /dev/null +++ b/ui/terminal/tui/lobbyui/help.go @@ -0,0 +1,91 @@ +package lobbyui + +import ( + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// keyMap defines a set of keybindings. To work for help it must satisfy +// key.Map. It could also very easily be a map[string]key.Binding. +type keyMap struct { + Up key.Binding + Down key.Binding + Left key.Binding + Right key.Binding + Help key.Binding + Quit key.Binding +} + +// ShortHelp returns keybindings to be shown in the mini help view. It's part +// of the key.Map interface. +func (k keyMap) ShortHelp() []key.Binding { + return []key.Binding{k.Help, k.Quit} +} + +// FullHelp returns keybindings for the expanded help view. It's part of the +// key.Map interface. +func (k keyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {k.Up, k.Down, k.Left, k.Right}, // first column + {k.Help, k.Quit}, // second column + } +} + +var keys = keyMap{ + Up: key.NewBinding( + key.WithKeys("up", "k"), + key.WithHelp("↑/k", "move up"), + ), + Down: key.NewBinding( + key.WithKeys("down", "j"), + key.WithHelp("↓/j", "move down"), + ), + Left: key.NewBinding( + key.WithKeys("left", "h"), + key.WithHelp("←/h", "move left"), + ), + Right: key.NewBinding( + key.WithKeys("right", "l"), + key.WithHelp("→/l", "move right"), + ), + Help: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "toggle help"), + ), + Quit: key.NewBinding( + key.WithKeys("ctrl+c"), + key.WithHelp("ctrl+c", "quit"), + ), +} + +type model struct { + keys keyMap + help help.Model + inputStyle lipgloss.Style +} + +func NewHelpModel() model { + return model{ + keys: keys, + help: help.New(), + inputStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#FF75B7")), + } +} +func (m model) Init() tea.Cmd { return nil } + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.keys.Help): + m.help.ShowAll = !m.help.ShowAll + } + } + return m, nil +} + +func (m model) View() string { + return m.help.View(m.keys) +} diff --git a/ui/terminal/tui/lobbyui/lobby.go b/ui/terminal/tui/lobbyui/lobby.go index 8b1e76fb..56bd9bae 100644 --- a/ui/terminal/tui/lobbyui/lobby.go +++ b/ui/terminal/tui/lobbyui/lobby.go @@ -4,22 +4,53 @@ import ( "encoding/json" "fmt" "net/http" + "os" + "strings" "time" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/gorilla/websocket" + "golang.org/x/term" +) + +const ( + // In real life situations we'd adjust the document to fit the width we've + // detected. In the case of this example we're hardcoding the width, and + // later using the detected width only to truncate in order to avoid jaggy + // wrapping. + width = 96 + + columnWidth = 30 ) // Styles -var baseStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")) +var ( + baseStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")) + + // Status Bar. + statusBarStyle = lipgloss.NewStyle(). + Foreground(lipgloss.AdaptiveColor{Light: "#343433", Dark: "#C1C6B2"}). + Background(lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#353533"}) + + statusStyle = lipgloss.NewStyle(). + Inherit(statusBarStyle). + Foreground(lipgloss.Color("#FFFDF5")). + Background(lipgloss.Color("#FF5F87")). + Padding(0, 1). + MarginRight(1) + + statusText = lipgloss.NewStyle().Inherit(statusBarStyle) + + messageText = lipgloss.NewStyle().Align(lipgloss.Left) + // Page + docStyle = lipgloss.NewStyle().Padding(1, 2, 1, 2) +) // Message types -type statusMsg int - type errMsg struct{ err error } type Session struct { @@ -27,6 +58,7 @@ type Session struct { Name string `json:"name"` // UserCount int `json:"userCount"` } + type jamsResp struct { Sessions []Session `json:"sessions"` } @@ -50,7 +82,7 @@ func FetchSessions(baseURL string) tea.Cmd { // Return the HTTP status code // as a message. if res.StatusCode >= 400 { - return statusMsg(res.StatusCode) + return errMsg{fmt.Errorf("could not get sessions: %d", res.StatusCode)} } decoder := json.NewDecoder(res.Body) var resp jamsResp @@ -63,7 +95,8 @@ type Model struct { wsURL string sessions []Session jamTable table.Model - status int + help tea.Model + loading bool err error } @@ -75,22 +108,16 @@ func (m Model) Init() tea.Cmd { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { - case statusMsg: - // The server returned a status message. Save it to our model. Also - // tell the Bubble Tea runtime we want to exit because we have nothing - // else to do. We'll still be able to render a final view with our - // status message. - m.status = int(msg) - cmds = append(cmds, tea.Quit) + case tea.WindowSizeMsg: + m.jamTable.SetWidth(msg.Width - 10) case jamsResp: m.sessions = msg.Sessions m.jamTable = makeJamsTable(m) m.jamTable.Focus() + m.loading = false case errMsg: - // There was an error. Note it in the model. And tell the runtime - // we're done and want to quit. + // There was an error. Note it in the model. m.err = msg - cmds = append(cmds, tea.Quit) case tea.KeyMsg: switch msg.String() { case tea.KeyEnter.String(): @@ -99,36 +126,73 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, jamConnect(m.wsURL, jamId)) } } - newJamTable, cmd := m.jamTable.Update(msg) + newJamTable, jtCmd := m.jamTable.Update(msg) m.jamTable = newJamTable - cmds = append(cmds, cmd) + + newHelp, hCmd := m.help.Update(msg) + m.help = newHelp + + cmds = append(cmds, jtCmd, hCmd) return m, tea.Batch(cmds...) } func (m Model) View() string { - // If there's an error, print it out and don't do anything else. + physicalWidth, _, _ := term.GetSize(int(os.Stdout.Fd())) + doc := strings.Builder{} + status := "" + + if m.loading { + status = "Fetching Jam Sessions..." + } + if m.err != nil { - return fmt.Sprintf("\nWe had some trouble: %v\n\n", m.err) + status = fmt.Sprintf("Error: %v!", m.err) + } + + // Jam Session Table + { + if len(m.sessions) > 0 { + jamTable := baseStyle.Width(width).Render(m.jamTable.View()) + doc.WriteString(jamTable) + } else if !m.loading { + doc.WriteString(messageText.Render("No Jams Yet. Create one?\n\n")) + } + } + // Status bar + { + w := lipgloss.Width + + statusKey := statusStyle.Render("STATUS") + statusVal := statusText.Copy(). + Width(width - w(statusKey)). + Render(status) + + bar := lipgloss.JoinHorizontal(lipgloss.Top, + statusKey, + statusVal, + ) + + doc.WriteString("\n" + statusBarStyle.Width(width).Render(bar)) } - // Tell the user we're doing something. - s := fmt.Sprintln("Fetching Jam Sessions...") - // When the server responds with a status, add it to the current line. - if m.status > 0 { - s += fmt.Sprintf("%d %s!", m.status, http.StatusText(m.status)) + // Help menu + { + doc.WriteString("\n" + m.help.View()) } - if m.sessions != nil { - s += baseStyle.Render(m.jamTable.View()) + "\n" + if physicalWidth > 0 { + docStyle = docStyle.MaxWidth(physicalWidth) } - // Send off whatever we came up with above for rendering. - return "\n" + s + "\n\n" + // Okay, let's print it + return docStyle.Render(doc.String()) } func New(wsURL string) tea.Model { return Model{ - wsURL: wsURL, + wsURL: wsURL, + help: NewHelpModel(), + loading: true, } } diff --git a/ui/terminal/tui/tui.go b/ui/terminal/tui/tui.go index 20496027..561d2760 100644 --- a/ui/terminal/tui/tui.go +++ b/ui/terminal/tui/tui.go @@ -118,7 +118,8 @@ func Run() { bail(err) } - if err := tea.NewProgram(m).Start(); err != nil { + p := tea.NewProgram(m, tea.WithAltScreen()) + if err := p.Start(); err != nil { bail(err) } } From b690d6ccf3888469e5aff95d75a53c1b79dbbf33 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Wed, 21 Sep 2022 21:35:36 -0400 Subject: [PATCH 057/246] Update help menu --- ui/terminal/tui/lobbyui/help.go | 25 ++++++++++++++++++------- ui/terminal/tui/lobbyui/lobby.go | 21 ++++++++++++--------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/ui/terminal/tui/lobbyui/help.go b/ui/terminal/tui/lobbyui/help.go index 1b66ad8f..d4b2564c 100644 --- a/ui/terminal/tui/lobbyui/help.go +++ b/ui/terminal/tui/lobbyui/help.go @@ -10,22 +10,26 @@ import ( // keyMap defines a set of keybindings. To work for help it must satisfy // key.Map. It could also very easily be a map[string]key.Binding. type keyMap struct { - Up key.Binding - Down key.Binding - Left key.Binding - Right key.Binding - Help key.Binding - Quit key.Binding + Up key.Binding + Down key.Binding + Left key.Binding + Right key.Binding + Refresh key.Binding + New key.Binding + Enter key.Binding + Help key.Binding + Quit key.Binding } // ShortHelp returns keybindings to be shown in the mini help view. It's part // of the key.Map interface. func (k keyMap) ShortHelp() []key.Binding { - return []key.Binding{k.Help, k.Quit} + return []key.Binding{k.Up, k.Down, k.New, k.Enter, k.Help, k.Quit} } // FullHelp returns keybindings for the expanded help view. It's part of the // key.Map interface. +// TODO: Figure out why FullHelp not rendering correctly func (k keyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ {k.Up, k.Down, k.Left, k.Right}, // first column @@ -50,6 +54,13 @@ var keys = keyMap{ key.WithKeys("right", "l"), key.WithHelp("→/l", "move right"), ), + Refresh: key.NewBinding( + key.WithKeys("r"), + key.WithHelp("r", "refresh")), + New: key.NewBinding(key.WithKeys("n"), + key.WithHelp("n", "new jam")), + Enter: key.NewBinding(key.WithKeys("enter", "space"), + key.WithHelp("enter", "select")), Help: key.NewBinding( key.WithKeys("?"), key.WithHelp("?", "toggle help"), diff --git a/ui/terminal/tui/lobbyui/lobby.go b/ui/terminal/tui/lobbyui/lobby.go index 56bd9bae..f6a4b708 100644 --- a/ui/terminal/tui/lobbyui/lobby.go +++ b/ui/terminal/tui/lobbyui/lobby.go @@ -46,6 +46,8 @@ var ( statusText = lipgloss.NewStyle().Inherit(statusBarStyle) messageText = lipgloss.NewStyle().Align(lipgloss.Left) + + helpMenu = lipgloss.NewStyle().Align(lipgloss.Center).PaddingTop(2) // Page docStyle = lipgloss.NewStyle().Padding(1, 2, 1, 2) ) @@ -100,6 +102,14 @@ type Model struct { err error } +func New(wsURL string) tea.Model { + return Model{ + wsURL: wsURL, + help: NewHelpModel(), + loading: true, + } +} + // Init needed to satisfy Model interface. It doesn't seem to be called on sub-models. func (m Model) Init() tea.Cmd { return nil @@ -177,7 +187,8 @@ func (m Model) View() string { // Help menu { - doc.WriteString("\n" + m.help.View()) + + doc.WriteString("\n" + helpMenu.Render(m.help.View())) } if physicalWidth > 0 { @@ -188,14 +199,6 @@ func (m Model) View() string { return docStyle.Render(doc.String()) } -func New(wsURL string) tea.Model { - return Model{ - wsURL: wsURL, - help: NewHelpModel(), - loading: true, - } -} - // https://github.com/rog-golang-buddies/rapidmidiex-research/issues/9#issuecomment-1204853876 func makeJamsTable(m Model) table.Model { columns := []table.Column{ From 81d58018f9b0aa9922a3906e03ae5be23fa4dea7 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Wed, 21 Sep 2022 21:51:46 -0400 Subject: [PATCH 058/246] Resolve merge conflicts --- api/errors.go | 6 +- api/routes.go | 82 ------- {www => api}/routes_test.go | 2 +- api/service.go | 29 ++- go.mod | 18 -- go.sum | 477 ------------------------------------ www/service.go | 61 ----- 7 files changed, 29 insertions(+), 646 deletions(-) rename {www => api}/routes_test.go (81%) delete mode 100644 www/service.go diff --git a/api/errors.go b/api/errors.go index d8ad7b83..8efbdd96 100644 --- a/api/errors.go +++ b/api/errors.go @@ -5,7 +5,7 @@ import ( ) var ( - ErrNoCookie = errors.New("api: cookie not found") - ErrSessionNotFound = errors.New("api: session not found") - ErrSessionExists = errors.New("api: session already exists") + ErrNoCookie = errors.New("www: cookie not found") + ErrSessionNotFound = errors.New("www: session not found") + ErrSessionExists = errors.New("www: session already exists") ) diff --git a/api/routes.go b/api/routes.go index f90d8948..70823bae 100644 --- a/api/routes.go +++ b/api/routes.go @@ -11,22 +11,12 @@ import ( "github.com/rog-golang-buddies/rapidmidiex/internal/suid" ) -<<<<<<< HEAD:www/routes.go func (s *Service) handleP2PComms() http.HandlerFunc { type response[T any] struct { MessageTyp rmx.MessageType `json:"type"` Data T `json:"data"` -======= -type ( - session struct { - Name string `json:"name,omitempty"` - ID suid.SUID `json:"id,omitempty"` - Users []suid.SUID `json:"users,omitempty"` - UserCount int `json:"userCount"` ->>>>>>> 0f864c5a2eca7b383b3222a841413f7f644d3541:api/routes.go } -<<<<<<< HEAD:www/routes.go type join struct { ID suid.SUID `json:"id"` SessionID suid.SUID `json:"sessionId"` @@ -36,66 +26,24 @@ type ( ID suid.SUID `json:"id"` SessionID suid.SUID `json:"sessionId"` Error any `json:"err"` -======= -func (s *Service) routes() { - // middleware - s.r.Use(middleware.Logger) - - // v0 - s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) - s.r.Get("/", s.indexHTML("ui/www/index.html")) - s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) - - // REST v1 - s.r.Get("/api/v1/jam", s.listSessions()) - s.r.Post("/api/v1/jam", s.createSession()) - s.r.Get("/api/v1/jam/{id}", s.getSessionData) - - // Websocket - s.r.Get("/ws/{id}", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) -} - -func (s *Service) handleJamSession() http.HandlerFunc { - type response struct { - MessageType rmx.MessageType `json:"type"` - ID suid.SUID `json:"id"` - SessionID suid.SUID `json:"sessionId"` ->>>>>>> 0f864c5a2eca7b383b3222a841413f7f644d3541:api/routes.go } return func(w http.ResponseWriter, r *http.Request) { c := r.Context().Value(upgradeKey).(*ws.Conn) defer func() { -<<<<<<< HEAD:www/routes.go // ! send error when Leaving session pool c.SendMessage(response[leave]{ MessageTyp: rmx.Leave, Data: leave{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, -======= - c.SendMessage(response{ - MessageType: rmx.Leave, - ID: c.ID.ShortUUID(), - SessionID: c.Pool().ID.ShortUUID(), ->>>>>>> 0f864c5a2eca7b383b3222a841413f7f644d3541:api/routes.go }) c.Close() }() -<<<<<<< HEAD:www/routes.go if err := c.SendMessage(response[join]{ MessageTyp: rmx.Join, Data: join{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, }); err != nil { -======= - err := c.SendMessage(response{ - MessageType: rmx.Join, - ID: c.ID.ShortUUID(), - SessionID: c.Pool().ID.ShortUUID(), - }) - - if err != nil { ->>>>>>> 0f864c5a2eca7b383b3222a841413f7f644d3541:api/routes.go s.l.Println(err) return } @@ -134,7 +82,6 @@ func (s *Service) handleCreateRoom() http.HandlerFunc { } } -<<<<<<< HEAD:www/routes.go func (s *Service) handleGetRoom() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, err := s.parseUUID(w, r) @@ -142,14 +89,6 @@ func (s *Service) handleGetRoom() http.HandlerFunc { s.respond(w, r, err, http.StatusBadRequest) return } -======= -func (s *Service) getSessionData(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r) - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } ->>>>>>> 0f864c5a2eca7b383b3222a841413f7f644d3541:api/routes.go // ! rename method as `Get` is nondescriptive p, err := s.c.Get(uid) @@ -158,17 +97,10 @@ func (s *Service) getSessionData(w http.ResponseWriter, r *http.Request) { return } -<<<<<<< HEAD:www/routes.go v := &Session{ ID: p.ID.ShortUUID(), Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), } -======= - v := &session{ - ID: p.ID.ShortUUID(), - Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), - } ->>>>>>> 0f864c5a2eca7b383b3222a841413f7f644d3541:api/routes.go s.respond(w, r, v, http.StatusOK) } @@ -180,20 +112,6 @@ func (s *Service) handleListRooms() http.HandlerFunc { } return func(w http.ResponseWriter, r *http.Request) { -<<<<<<< HEAD:www/routes.go -======= - pl := s.c.List() - - sl := make([]session, 0, len(pl)) - for _, p := range pl { - sl = append(sl, session{ - Name: "", // name not implemented yet - ID: p.ID.ShortUUID(), - UserCount: p.Size(), - }) - } - ->>>>>>> 0f864c5a2eca7b383b3222a841413f7f644d3541:api/routes.go v := &response{ Sessions: rmx.FMap(s.c.List(), func(p *ws.Pool) Session { return Session{ID: p.ID.ShortUUID()} }), } diff --git a/www/routes_test.go b/api/routes_test.go similarity index 81% rename from www/routes_test.go rename to api/routes_test.go index 58b25ff5..7f90bc7d 100644 --- a/www/routes_test.go +++ b/api/routes_test.go @@ -1,4 +1,4 @@ -package www +package api import "testing" diff --git a/api/service.go b/api/service.go index dc5e72e0..3bfe563d 100644 --- a/api/service.go +++ b/api/service.go @@ -4,6 +4,7 @@ import ( "log" "net/http" + "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" h "github.com/hyphengolang/prelude/http" @@ -19,7 +20,7 @@ type Service struct { c *ws.Client } -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } +func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } func NewService(r chi.Router) *Service { s := &Service{r, log.Default(), ws.DefaultClient} @@ -27,14 +28,34 @@ func NewService(r chi.Router) *Service { return s } -func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { +func (s Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { h.Respond(w, r, data, status) } -func (s *Service) fileServer(prefix string, dirname string) http.Handler { +func (s Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { + return h.Decode(w, r, data) +} + +func (s Service) fileServer(prefix string, dirname string) http.Handler { return h.FileServer(prefix, dirname) } -func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { +func (S Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { return suid.ParseString(chi.URLParam(r, "id")) } + +func (s *Service) routes() { + // middleware + s.r.Use(middleware.Logger) + + // temporary static files + s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) + s.r.Get("/", s.indexHTML("ui/www/index.html")) + s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) + + // v1 + s.r.Get("/api/v1/jam", s.handleListRooms()) + s.r.Post("/api/v1/jam", s.handleCreateRoom()) + s.r.Get("/api/v1/jam/{id}", s.handleGetRoom()) + s.r.Get("/api/v1/jam/{id}/ws", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.wsConnectionPool)) +} diff --git a/go.mod b/go.mod index 0341f6b4..607bf87c 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/hyphengolang/prelude v0.0.7 github.com/rs/cors v1.8.2 - github.com/spf13/viper v1.12.0 golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 ) @@ -32,27 +31,10 @@ require ( ) require ( - github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/lithammer/shortuuid/v4 v4.0.0 - github.com/magiconair/properties v1.8.6 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.1 // indirect - github.com/rogpeppe/go-internal v1.8.0 // indirect - github.com/spf13/afero v1.8.2 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/testify v1.8.0 // indirect - github.com/subosito/gotenv v1.3.0 // indirect golang.org/x/exp v0.0.0-20220914170420-dc92f8653013 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect golang.org/x/text v0.3.7 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/ini.v1 v1.66.4 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d68f9b05..a6afa8b2 100644 --- a/go.sum +++ b/go.sum @@ -1,45 +1,4 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/charmbracelet/bubbles v0.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og= github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4= @@ -49,124 +8,29 @@ github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJ github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY= github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/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.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hyphengolang/prelude v0.0.7 h1:AgLkbASkg/3ioIjr0JinhReTiaeppIaX2sa7vrFwaoc= github.com/hyphengolang/prelude v0.0.7/go.mod h1:obKnO4SQhPfu3gqA9g29nbDRQLNTtmJy02i7KYZGdfM= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -176,8 +40,6 @@ github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= @@ -190,365 +52,26 @@ github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= -github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= -github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= -github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= -github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220914170420-dc92f8653013 h1:ZjglnWxEUdPyXl4o/j4T89SRCI+4X6NW6185PNLEOF4= golang.org/x/exp v0.0.0-20220914170420-dc92f8653013/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= -gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/www/service.go b/www/service.go deleted file mode 100644 index d1173b90..00000000 --- a/www/service.go +++ /dev/null @@ -1,61 +0,0 @@ -package www - -import ( - "log" - "net/http" - - "github.com/go-chi/chi/middleware" - "github.com/go-chi/chi/v5" - - h "github.com/hyphengolang/prelude/http" - - "github.com/rog-golang-buddies/rapidmidiex/internal/suid" - ws "github.com/rog-golang-buddies/rapidmidiex/www/websocket" -) - -type Service struct { - r chi.Router - l *log.Logger - - c *ws.Client -} - -func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } - -func NewService(r chi.Router) *Service { - s := &Service{r, log.Default(), ws.DefaultClient} - s.routes() - return s -} - -func (s Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { - h.Respond(w, r, data, status) -} - -func (s Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { - return h.Decode(w, r, data) -} - -func (s Service) fileServer(prefix string, dirname string) http.Handler { - return h.FileServer(prefix, dirname) -} - -func (S Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { - return suid.ParseString(chi.URLParam(r, "id")) -} - -func (s *Service) routes() { - // middleware - s.r.Use(middleware.Logger) - - // temporary static files - s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) - s.r.Get("/", s.indexHTML("ui/www/index.html")) - s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) - - // v1 - s.r.Get("/api/v1/jam", s.handleListRooms()) - s.r.Post("/api/v1/jam", s.handleCreateRoom()) - s.r.Get("/api/v1/jam/{id}", s.handleGetRoom()) - s.r.Get("/api/v1/jam/{id}/ws", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.wsConnectionPool)) -} From 06e741d70a5a92e8dac5fb1e987c8bd47fafdab9 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Wed, 21 Sep 2022 21:59:42 -0400 Subject: [PATCH 059/246] Fix Service receivers --- api/errors.go | 6 +++--- api/service.go | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/errors.go b/api/errors.go index 8efbdd96..d8ad7b83 100644 --- a/api/errors.go +++ b/api/errors.go @@ -5,7 +5,7 @@ import ( ) var ( - ErrNoCookie = errors.New("www: cookie not found") - ErrSessionNotFound = errors.New("www: session not found") - ErrSessionExists = errors.New("www: session already exists") + ErrNoCookie = errors.New("api: cookie not found") + ErrSessionNotFound = errors.New("api: session not found") + ErrSessionExists = errors.New("api: session already exists") ) diff --git a/api/service.go b/api/service.go index 3bfe563d..7aab9804 100644 --- a/api/service.go +++ b/api/service.go @@ -20,7 +20,7 @@ type Service struct { c *ws.Client } -func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } func NewService(r chi.Router) *Service { s := &Service{r, log.Default(), ws.DefaultClient} @@ -28,19 +28,19 @@ func NewService(r chi.Router) *Service { return s } -func (s Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { +func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { h.Respond(w, r, data, status) } -func (s Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { +func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { return h.Decode(w, r, data) } -func (s Service) fileServer(prefix string, dirname string) http.Handler { +func (s *Service) fileServer(prefix string, dirname string) http.Handler { return h.FileServer(prefix, dirname) } -func (S Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { +func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { return suid.ParseString(chi.URLParam(r, "id")) } From f23b5a3a4aab8c0f343ca8e27e56041582fe06e7 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 22 Sep 2022 07:33:20 +0100 Subject: [PATCH 060/246] clean up conflicts --- api/api.go | 2 + api/middleware.go | 2 +- api/routes.go | 151 ++---------- api/service.go | 19 ++ api/views.go | 40 ++++ api/websocket/conn.go | 4 +- cmd/env_test.go | 11 + cmd/main.go | 18 ++ go.mod | 21 -- go.sum | 484 --------------------------------------- internal/rmx.go | 8 +- internal/tmp/errors.go | 29 --- internal/tmp/jam_test.go | 106 --------- internal/tmp/jam_tmp.go | 379 ------------------------------ internal/tmp/utils.go | 34 --- www/routes_test.go | 7 - www/service.go | 61 ----- 17 files changed, 111 insertions(+), 1265 deletions(-) create mode 100644 api/views.go create mode 100644 cmd/env_test.go delete mode 100644 internal/tmp/errors.go delete mode 100644 internal/tmp/jam_test.go delete mode 100644 internal/tmp/jam_tmp.go delete mode 100644 internal/tmp/utils.go delete mode 100644 www/routes_test.go delete mode 100644 www/service.go diff --git a/api/api.go b/api/api.go index 54f65b3b..b0f4baa8 100644 --- a/api/api.go +++ b/api/api.go @@ -21,6 +21,8 @@ type Session struct { ID suid.SUID `json:"id"` Name string `json:"name,omitempty"` Users []suid.SUID `json:"users,omitempty"` + /* Not really required */ + UserCount int `json:"userCount"` } type User struct { diff --git a/api/middleware.go b/api/middleware.go index 84f6e3ab..04b4a937 100644 --- a/api/middleware.go +++ b/api/middleware.go @@ -9,7 +9,7 @@ import ( ws "github.com/rog-golang-buddies/rapidmidiex/api/websocket" ) -func (s Service) wsConnectionPool(f http.HandlerFunc) http.HandlerFunc { +func (s Service) connectionPool(f http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, err := s.parseUUID(w, r) if err != nil { diff --git a/api/routes.go b/api/routes.go index f90d8948..93f0a846 100644 --- a/api/routes.go +++ b/api/routes.go @@ -4,29 +4,17 @@ import ( "encoding/json" "net/http" - t "github.com/hyphengolang/prelude/template" - ws "github.com/rog-golang-buddies/rapidmidiex/api/websocket" rmx "github.com/rog-golang-buddies/rapidmidiex/internal" "github.com/rog-golang-buddies/rapidmidiex/internal/suid" ) -<<<<<<< HEAD:www/routes.go -func (s *Service) handleP2PComms() http.HandlerFunc { +func (s Service) handleP2PComms() http.HandlerFunc { type response[T any] struct { - MessageTyp rmx.MessageType `json:"type"` - Data T `json:"data"` -======= -type ( - session struct { - Name string `json:"name,omitempty"` - ID suid.SUID `json:"id,omitempty"` - Users []suid.SUID `json:"users,omitempty"` - UserCount int `json:"userCount"` ->>>>>>> 0f864c5a2eca7b383b3222a841413f7f644d3541:api/routes.go + Typ rmx.MessageTyp `json:"type"` + Payload T `json:"payload"` } -<<<<<<< HEAD:www/routes.go type join struct { ID suid.SUID `json:"id"` SessionID suid.SUID `json:"sessionId"` @@ -35,67 +23,23 @@ type ( type leave struct { ID suid.SUID `json:"id"` SessionID suid.SUID `json:"sessionId"` - Error any `json:"err"` -======= -func (s *Service) routes() { - // middleware - s.r.Use(middleware.Logger) - - // v0 - s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) - s.r.Get("/", s.indexHTML("ui/www/index.html")) - s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) - - // REST v1 - s.r.Get("/api/v1/jam", s.listSessions()) - s.r.Post("/api/v1/jam", s.createSession()) - s.r.Get("/api/v1/jam/{id}", s.getSessionData) - - // Websocket - s.r.Get("/ws/{id}", chain(s.handleJamSession(), s.upgradeHTTP, s.sessionPool)) -} - -func (s *Service) handleJamSession() http.HandlerFunc { - type response struct { - MessageType rmx.MessageType `json:"type"` - ID suid.SUID `json:"id"` - SessionID suid.SUID `json:"sessionId"` ->>>>>>> 0f864c5a2eca7b383b3222a841413f7f644d3541:api/routes.go } return func(w http.ResponseWriter, r *http.Request) { c := r.Context().Value(upgradeKey).(*ws.Conn) defer func() { -<<<<<<< HEAD:www/routes.go - // ! send error when Leaving session pool - c.SendMessage(response[leave]{ - MessageTyp: rmx.Leave, - Data: leave{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, -======= - c.SendMessage(response{ - MessageType: rmx.Leave, - ID: c.ID.ShortUUID(), - SessionID: c.Pool().ID.ShortUUID(), ->>>>>>> 0f864c5a2eca7b383b3222a841413f7f644d3541:api/routes.go + c.SendMessage(response[join]{ + Typ: rmx.Leave, + Payload: join{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, }) c.Close() }() -<<<<<<< HEAD:www/routes.go - if err := c.SendMessage(response[join]{ - MessageTyp: rmx.Join, - Data: join{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, + if err := c.SendMessage(response[leave]{ + Typ: rmx.Join, + Payload: leave{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, }); err != nil { -======= - err := c.SendMessage(response{ - MessageType: rmx.Join, - ID: c.ID.ShortUUID(), - SessionID: c.Pool().ID.ShortUUID(), - }) - - if err != nil { ->>>>>>> 0f864c5a2eca7b383b3222a841413f7f644d3541:api/routes.go s.l.Println(err) return } @@ -112,7 +56,7 @@ func (s *Service) handleJamSession() http.HandlerFunc { // * here the message will be passed off to a different handler // * via a go routine* - if err := c.SendMessage(response[int]{MessageTyp: rmx.Message, Data: 10}); err != nil { + if err := c.SendMessage(response[int]{Typ: rmx.Message, Payload: 10}); err != nil { s.l.Println(err) return } @@ -120,7 +64,7 @@ func (s *Service) handleJamSession() http.HandlerFunc { } } -func (s *Service) handleCreateRoom() http.HandlerFunc { +func (s Service) handleCreateRoom() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, err := s.c.NewPool() if err != nil { @@ -134,22 +78,13 @@ func (s *Service) handleCreateRoom() http.HandlerFunc { } } -<<<<<<< HEAD:www/routes.go -func (s *Service) handleGetRoom() http.HandlerFunc { +func (s Service) handleGetRoom() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, err := s.parseUUID(w, r) if err != nil { s.respond(w, r, err, http.StatusBadRequest) return } -======= -func (s *Service) getSessionData(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r) - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } ->>>>>>> 0f864c5a2eca7b383b3222a841413f7f644d3541:api/routes.go // ! rename method as `Get` is nondescriptive p, err := s.c.Get(uid) @@ -158,83 +93,25 @@ func (s *Service) getSessionData(w http.ResponseWriter, r *http.Request) { return } -<<<<<<< HEAD:www/routes.go v := &Session{ ID: p.ID.ShortUUID(), Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), } -======= - v := &session{ - ID: p.ID.ShortUUID(), - Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), - } ->>>>>>> 0f864c5a2eca7b383b3222a841413f7f644d3541:api/routes.go s.respond(w, r, v, http.StatusOK) } } -func (s *Service) handleListRooms() http.HandlerFunc { +func (s Service) handleListRooms() http.HandlerFunc { type response struct { Sessions []Session `json:"sessions"` } return func(w http.ResponseWriter, r *http.Request) { -<<<<<<< HEAD:www/routes.go -======= - pl := s.c.List() - - sl := make([]session, 0, len(pl)) - for _, p := range pl { - sl = append(sl, session{ - Name: "", // name not implemented yet - ID: p.ID.ShortUUID(), - UserCount: p.Size(), - }) - } - ->>>>>>> 0f864c5a2eca7b383b3222a841413f7f644d3541:api/routes.go v := &response{ - Sessions: rmx.FMap(s.c.List(), func(p *ws.Pool) Session { return Session{ID: p.ID.ShortUUID()} }), + Sessions: rmx.FMap(s.c.List(), func(p *ws.Pool) Session { return Session{ID: p.ID.ShortUUID(), UserCount: p.Size()} }), } s.respond(w, r, v, http.StatusOK) } } - -// ! to be discarded - -func (s *Service) indexHTML(path string) http.HandlerFunc { - render, err := t.Render(path) - if err != nil { - panic(err) - } - - return func(w http.ResponseWriter, r *http.Request) { - render(w, r, nil) - } -} - -func (s *Service) jamSessionHTML(path string) http.HandlerFunc { - render, err := t.Render(path) - if err != nil { - panic(err) - } - - // ! I should be rendering a 404 page if there is an error - // ! in this layer, but for an MVC this will do - return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r) - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - if _, err = s.c.Get(uid); err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - render(w, r, nil) - } -} diff --git a/api/service.go b/api/service.go index dc5e72e0..22a67f8f 100644 --- a/api/service.go +++ b/api/service.go @@ -4,6 +4,7 @@ import ( "log" "net/http" + "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" h "github.com/hyphengolang/prelude/http" @@ -38,3 +39,21 @@ func (s *Service) fileServer(prefix string, dirname string) http.Handler { func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { return suid.ParseString(chi.URLParam(r, "id")) } + +func (s *Service) routes() { + // middleware + s.r.Use(middleware.Logger) + + // static file + s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) + s.r.Get("/", s.indexHTML("ui/www/index.html")) + s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) + + // REST v1 + s.r.Get("/api/v1/jam", s.handleListRooms()) + s.r.Post("/api/v1/jam", s.handleCreateRoom()) + s.r.Get("/api/v1/jam/{id}", s.handleGetRoom()) + + // Websocket + s.r.Get("/ws/{id}", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.connectionPool)) +} diff --git a/api/views.go b/api/views.go new file mode 100644 index 00000000..eade241d --- /dev/null +++ b/api/views.go @@ -0,0 +1,40 @@ +package api + +import "net/http" +import t "github.com/hyphengolang/prelude/template" +// ! to be discarded + +func (s *Service) indexHTML(path string) http.HandlerFunc { + render, err := t.Render(path) + if err != nil { + panic(err) + } + + return func(w http.ResponseWriter, r *http.Request) { + render(w, r, nil) + } +} + +func (s *Service) jamSessionHTML(path string) http.HandlerFunc { + render, err := t.Render(path) + if err != nil { + panic(err) + } + + // ! I should be rendering a 404 page if there is an error + // ! in this layer, but for an MVC this will do + return func(w http.ResponseWriter, r *http.Request) { + uid, err := s.parseUUID(w, r) + if err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + if _, err = s.c.Get(uid); err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + render(w, r, nil) + } +} diff --git a/api/websocket/conn.go b/api/websocket/conn.go index f9071b98..96932c70 100644 --- a/api/websocket/conn.go +++ b/api/websocket/conn.go @@ -30,9 +30,9 @@ func (c Conn) SendMessage(v any) error { return nil } -func (c Conn) SendMessage2(typ rmx.MessageType, data any) error { +func (c Conn) SendMessage2(typ rmx.MessageTyp, data any) error { v := struct { - Typ rmx.MessageType + Typ rmx.MessageTyp }{} c.p.msgs <- v return nil diff --git a/cmd/env_test.go b/cmd/env_test.go new file mode 100644 index 00000000..bf433afe --- /dev/null +++ b/cmd/env_test.go @@ -0,0 +1,11 @@ +package main + +import ( + "testing" + + "github.com/spf13/viper" +) + +func TestEnv(t *testing.T) { + viper.SetConfigFile(".env") +} diff --git a/cmd/main.go b/cmd/main.go index 86f664d6..7e466684 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -91,3 +91,21 @@ func init() { // panic(err) // } } + +// func LoadConfig(path string) (config Config, err error) { +// // Read file path +// viper.AddConfigPath(path) +// // set config file and path +// viper.SetConfigName("app") +// viper.SetConfigType("env") +// // watching changes in app.env +// viper.AutomaticEnv() +// // reading the config file +// err = viper.ReadInConfig() +// if err != nil { +// return +// } + +// err = viper.Unmarshal(&config) +// return +// } diff --git a/go.mod b/go.mod index 0341f6b4..a75d09f3 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,10 @@ require ( github.com/charmbracelet/lipgloss v0.6.0 github.com/go-chi/chi v1.5.4 github.com/go-chi/chi/v5 v5.0.7 - github.com/gobwas/ws v1.1.0 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 github.com/hyphengolang/prelude v0.0.7 github.com/rs/cors v1.8.2 - github.com/spf13/viper v1.12.0 golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 ) @@ -32,27 +30,8 @@ require ( ) require ( - github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/gobwas/httphead v0.1.0 // indirect - github.com/gobwas/pool v0.2.1 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/lithammer/shortuuid/v4 v4.0.0 - github.com/magiconair/properties v1.8.6 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.1 // indirect - github.com/rogpeppe/go-internal v1.8.0 // indirect - github.com/spf13/afero v1.8.2 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/testify v1.8.0 // indirect - github.com/subosito/gotenv v1.3.0 // indirect golang.org/x/exp v0.0.0-20220914170420-dc92f8653013 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect golang.org/x/text v0.3.7 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/ini.v1 v1.66.4 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d68f9b05..725a06de 100644 --- a/go.sum +++ b/go.sum @@ -1,45 +1,4 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/charmbracelet/bubbles v0.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og= github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4= @@ -49,124 +8,23 @@ github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJ github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY= github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/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.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= -github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= -github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hyphengolang/prelude v0.0.7 h1:AgLkbASkg/3ioIjr0JinhReTiaeppIaX2sa7vrFwaoc= github.com/hyphengolang/prelude v0.0.7/go.mod h1:obKnO4SQhPfu3gqA9g29nbDRQLNTtmJy02i7KYZGdfM= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -176,8 +34,6 @@ github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= @@ -190,365 +46,25 @@ github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= -github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= -github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= -github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= -github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220914170420-dc92f8653013 h1:ZjglnWxEUdPyXl4o/j4T89SRCI+4X6NW6185PNLEOF4= golang.org/x/exp v0.0.0-20220914170420-dc92f8653013/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= -gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/rmx.go b/internal/rmx.go index e549eb36..3913d3e1 100644 --- a/internal/rmx.go +++ b/internal/rmx.go @@ -1,6 +1,6 @@ package internal -type MessageType int +type MessageTyp int const ( Unknown = iota @@ -16,7 +16,7 @@ const ( NoteOff ) -func (t MessageType) String() string { +func (t MessageTyp) String() string { switch t { case Create: return "CREATE" @@ -38,7 +38,7 @@ func (t MessageType) String() string { } } -func (t *MessageType) UnmarshalJSON(b []byte) error { +func (t *MessageTyp) UnmarshalJSON(b []byte) error { switch s := string(b[1 : len(b)-1]); s { case "CREATE": *t = Create @@ -61,7 +61,7 @@ func (t *MessageType) UnmarshalJSON(b []byte) error { return nil } -func (t MessageType) MarshalJSON() ([]byte, error) { +func (t MessageTyp) MarshalJSON() ([]byte, error) { return []byte(`"` + t.String() + `"`), nil } diff --git a/internal/tmp/errors.go b/internal/tmp/errors.go deleted file mode 100644 index 39717df4..00000000 --- a/internal/tmp/errors.go +++ /dev/null @@ -1,29 +0,0 @@ -// * most of this isn't required as I have already got a function that handles http responses -// * file:jam_tmp.go is dependant therefore cannot delete this straight away -package tmp - -import ( - "errors" - "log" - "net/http" -) - -var ( - ErrTodo = errors.New("rmx: not yet implemented") - - errNoCookie = errorResponse{status: http.StatusUnauthorized, message: "Cookie not found."} - errSessionNotFound = errorResponse{status: http.StatusNotFound, message: "Session not found."} - errSessionExists = errorResponse{status: http.StatusNotFound, message: "Session already exists."} -) - -func handlerError(w http.ResponseWriter, err error) { - if err != nil { - if httpError, ok := err.(*errorResponse); ok { - http.Error(w, httpError.message, httpError.status) - return - } - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} diff --git a/internal/tmp/jam_test.go b/internal/tmp/jam_test.go deleted file mode 100644 index e51cf62a..00000000 --- a/internal/tmp/jam_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// * Will be adding the testing back once we can adapt it for the `gorilla/websocket` package -package tmp - -import ( - "context" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/gobwas/ws" - "github.com/gobwas/ws/wsutil" -) - -func TestConnect(t *testing.T) { - t.Run("we get a list of available Jam Session when we connect over websocket", func(t *testing.T) { - - js := NewJamService() - server := httptest.NewServer(NewServer(js).Router) - - wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws/v1/jam" - - conn, _, _, err := ws.DefaultDialer.Dial(context.Background(), wsURL) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - defer server.Close() - defer conn.Close() - - listJamsReq := []byte(`{"messageType": "JAM_LIST"}`) - _, err = conn.Write(listJamsReq) - if err != nil { - t.Fatal("could not write to WS connection") - } - - within(t, time.Millisecond*10, func() { - msg, err := wsutil.ReadServerText(conn) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - want := `{"Jams":[]}` + "\n" - if string(msg) != want { - t.Errorf(`got "%s", want "%s"`, string(msg), want) - } - }) - }) -} - -func TestJamConnect(t *testing.T) { - t.Run("we receive a welcome message when entering a Jam", func(t *testing.T) { - // Hardcoding the Jam ID for now. - // In the future we'll connect through a "JAM_JOIN" message sequence. - // An empty payload denotes the client wants to join any available Jam. - // Req: { "messageType": "JAM_JOIN", "payload": {} } → server - // Response: client ← { "jamId": "123456789" } - // Client makes a new ws request @ /ws/v1/jam/123456789 - jamId := "123456789" - - js := NewJamService() - server := httptest.NewServer(NewServer(js).Router) - wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws/v1/jam/" + jamId - - conn, _, _, err := ws.DefaultDialer.Dial(context.Background(), wsURL) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - defer server.Close() - defer conn.Close() - - listJamsReq := []byte(`{"messageType": "JAM_HELLO"}`) - _, err = conn.Write(listJamsReq) - if err != nil { - t.Fatal("could not write to WS connection") - } - - within(t, time.Millisecond*10, func() { - msg, err := wsutil.ReadServerText(conn) - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - want := `{"messageText":"Welcome to Jam 123456789!"}` + "\n" - if string(msg) != want { - t.Errorf(`got "%s", want "%s"`, string(msg), want) - } - }) - }) -} - -func within(t testing.TB, d time.Duration, assert func()) { - t.Helper() - - done := make(chan struct{}, 1) - - go func() { - assert() - done <- struct{}{} - }() - - select { - case <-time.After(d): - t.Error("timed out") - case <-done: - } -} diff --git a/internal/tmp/jam_tmp.go b/internal/tmp/jam_tmp.go deleted file mode 100644 index 476258f1..00000000 --- a/internal/tmp/jam_tmp.go +++ /dev/null @@ -1,379 +0,0 @@ -// * In the process of taking useful code from here and moving to the `api` package -package tmp - -import ( - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "sync" - - "github.com/go-chi/chi/v5" - "github.com/gobwas/ws" - "github.com/gobwas/ws/wsutil" - "github.com/google/uuid" -) - -// struct type to store info related to a websocket connection -type jamConn struct { - mu sync.Mutex - conn io.ReadWriter - - id string - username string -} - -func (c *jamConn) write(i interface{}) error { - w := wsutil.NewWriter(c.conn, ws.StateServerSide, ws.OpText) - encoder := json.NewEncoder(w) - - c.mu.Lock() - defer c.mu.Unlock() - - if err := encoder.Encode(i); err != nil { - return err - } - - return w.Flush() -} - -func (c *jamConn) writeRaw(b []byte) error { - c.mu.Lock() - defer c.mu.Unlock() - - _, err := c.conn.Write(b) - return err -} - -// struct type to store info related to a Jam session -// also contains a map of current connections to the session -type jamSession struct { - mu sync.RWMutex - conns map[string]*jamConn - out chan []interface{} - - id string - name string - tempo uint - owner string -} - -func (s *jamSession) addConn(jc *jamConn) { - s.mu.Lock() - s.conns[jc.id] = jc - s.mu.Unlock() -} - -// func (s *jamSession) writer(i interface{}) { -// for bts := range c.out { -// s.mu.RLock() -// cs := s.conns -// s.mu.RUnlock() -// -// for _, c := range cs { -// c := c // For closure. -// c.writeRaw(bts) -// } -// } -// } - -// * I have adapted this slightly inside the `api/ws` package -// iterates through session connections -// and send provided message to each of them -func (s *jamSession) broadcast(i interface{}) { - for _, c := range s.conns { - select { - case <-s.out: - c.write(i) - default: - delete(s.conns, c.username) - // c.close() - } - } -} - -// * Imo, a service should have a `ServeHTTP` method attached if it is going to be talking -// * directly to the web, I have adapted this inside `api` package -// * I have also decoupled the RESTful logic with sessions into a separate data structure -// struct type for the Jam service -// also contains current available sessions created by users -type JamService struct { - mu sync.RWMutex - sessions map[string]*jamSession -} - -// * Better to define some of these inside the handlers themselves -// * An example of such pattern can be found inside the `api` package -// request types -type ( - newJamReq struct { - Username string `json:"username"` - SessionName string `json:"session_name"` - Tempo uint `json:"tempo"` - } - - joinJamReq struct { - Username string `json:"username"` - SessionID string `json:"session_id"` - } - wsReq struct { - MessageType string `json:"messageType"` // Type of application message. Ex: JAM_SESSION_CONNECT, JAM_SESSION_CREATE - Payload struct{} `json:"payload"` // Payload of message, differs according to MessageType. Ex: JAM_SESSION_CREATE may contain Jam Session config (tempo, session name, etc). - } - - JamSlim struct { - Id string `json:"id"` - Name string `json:"name"` - } - listJamsResp struct { - Jams []JamSlim - } -) - -func NewJamService() *JamService { - return &JamService{ - sessions: make(map[string]*jamSession), - } -} - -// new session handler -func (s *JamService) NewSession(w http.ResponseWriter, r *http.Request) { - // get values from the request - si := newJamReq{} - if err := parse(r, &si); err != nil { - log.Println(err) - w.WriteHeader(http.StatusBadRequest) - return - } - - // upgrade http connection to websocket - conn, _, _, err := ws.UpgradeHTTP(r, w) - if err != nil { - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // create a new connection between the owner and the server - // then add it to the session connections - c := jamConn{} - c.conn = conn - c.id = uuid.NewString() - if isEmptyString(si.Username) { - c.username = c.id - } else { - c.username = si.Username - } - - // create a new session and set the session owner - session := jamSession{ - conns: make(map[string]*jamConn), - out: make(chan []interface{}), - - id: uuid.NewString(), - name: si.SessionName, - tempo: si.Tempo, - owner: c.id, - } - // add session to sessions map - s.addSession(&session) - // check for errors - // err isn't nil if the username is already used - session.addConn(&c) - session.broadcast("Welcome to Rapidmidiex!") - - w.WriteHeader(http.StatusOK) -} - -// Connect establishes a WebSocket connection with the application. From there we can communicate with the client about which session to join. -func (s *JamService) Connect(w http.ResponseWriter, r *http.Request) { - // upgrade http connection to websocket - conn, _, _, err := ws.UpgradeHTTP(r, w) - if err != nil { - log.Println("connect", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - go func() { - defer conn.Close() - var ( - fr = wsutil.NewReader(conn, ws.StateServerSide) - fw = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText) - decoder = json.NewDecoder(fr) - encoder = json.NewEncoder(fw) - ) - for { - hdr, err := fr.NextFrame() - if err != nil { - log.Println(err) - } - if hdr.OpCode == ws.OpClose { - log.Println(io.EOF) - // Break out of for and close the connection - return - } - - // TODO: Hand off messages to some app level message broker - // Ex: JAM_SESSION_CONNECT, JAM_SESSION_CREATE - var req wsReq - if err := decoder.Decode(&req); err != nil { - log.Println(err) - } - - resp := listJamsResp{Jams: make([]JamSlim, 0)} - if err := encoder.Encode(&resp); err != nil { - log.Println(err) - } - if err = fw.Flush(); err != nil { - log.Println(err) - } - } - }() -} - -func (s *JamService) Join(w http.ResponseWriter, r *http.Request) { - // upgrade http connection to websocket - conn, _, _, err := ws.UpgradeHTTP(r, w) - if err != nil { - log.Println("connect", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - jamId := chi.URLParam(r, "jamId") - log.Println("jamId***", jamId) - - // Hardcoded new session for now - session := &jamSession{ - conns: make(map[string]*jamConn), - out: make(chan []interface{}), - - id: jamId, - name: "Jam On It!", - tempo: 85, - owner: uuid.NewString(), - } - - // Since the session is hardcoded, it probably already exists. - err = s.addSession(session) - // If the session does exist, we can ignore and keep rolling. - if err != nil && err != &errSessionExists { - handlerError(w, err) - return - } - - session, err = s.getSession(jamId) - if err != nil { - handlerError(w, err) - return - } - - go func() { - defer conn.Close() - var ( - fr = wsutil.NewReader(conn, ws.StateServerSide) - fw = wsutil.NewWriter(conn, ws.StateServerSide, ws.OpText) - decoder = json.NewDecoder(fr) - encoder = json.NewEncoder(fw) - ) - for { - hdr, err := fr.NextFrame() - if err != nil { - log.Println("NextFrame error", err) - } - if hdr.OpCode == ws.OpClose { - // Break out of for and close the connection - return - } - - // TODO: Hand off messages to some Jam level message handler - // Ex: NOTE_ON, NOTE_OFF, PLAYER_JOINED, PLAYER_MESSAGE - - // Should the message handler do the encoding/decoding since they will know which concrete type to decode into? - // TODO: Move all out to own handler - var req wsReq - if err := decoder.Decode(&req); err != nil { - log.Println(err) - } - - msg := fmt.Sprintf("Welcome to Jam %s!", session.id) - log.Println(msg) - // TODO: Create proper response types - type Resp struct { - MessageText string `json:"messageText"` - } - - resp := Resp{MessageText: msg} - if err := encoder.Encode(&resp); err != nil { - log.Println(err) - } - if err = fw.Flush(); err != nil { - log.Println(err) - } - } - }() -} - -// join session handler -func (s *JamService) JoinSession(w http.ResponseWriter, r *http.Request) { - // get values from the request - ji := joinJamReq{} - if err := parse(r, &ji); err != nil { - log.Println(err) - w.WriteHeader(http.StatusBadRequest) - return - } - - sID := chi.URLParam(r, "session_id") - - // err isn't nil if session doesn't exist - session, err := s.getSession(sID) - if err != nil { - handlerError(w, err) - return - } - - // upgrade http connection to websocket - conn, _, _, err := ws.UpgradeHTTP(r, w) - if err != nil { - log.Println(err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // create a new connection - c := jamConn{} - c.conn = conn - c.id = uuid.NewString() - if isEmptyString(ji.Username) { - c.username = c.id - } else { - c.username = ji.Username - } - session.addConn(&c) - session.broadcast("Welcome " + ji.Username + "!") - - w.WriteHeader(http.StatusOK) -} - -func (s *JamService) addSession(js *jamSession) error { - s.mu.Lock() - _, err := s.getSession(js.id) - if err != nil && err != &errSessionNotFound { - return &errSessionExists - } - s.sessions[js.id] = js - s.mu.Unlock() - return nil -} - -func (s *JamService) getSession(sID string) (*jamSession, error) { - session, ok := s.sessions[sID] - if !ok { - return &jamSession{}, &errSessionNotFound - } - - return session, nil -} diff --git a/internal/tmp/utils.go b/internal/tmp/utils.go deleted file mode 100644 index 79ea843c..00000000 --- a/internal/tmp/utils.go +++ /dev/null @@ -1,34 +0,0 @@ -// * This file is not being used but saving as I have questions regarding the `isEmptyString` function -// * Parse not required as is the case with the errorResponse as those should really live in the `api` package -// * As there are function that depend on these utilities, I have refrained from deleting -package tmp - -import ( - "encoding/json" - "net/http" - "strings" -) - -func isEmptyString(s string) bool { - return len(strings.TrimSpace(s)) == 0 -} - -func parse(r *http.Request, out interface{}) error { - decoder := json.NewDecoder(r.Body) - err := decoder.Decode(&out) - if err != nil { - return err - } - - return nil -} - -type errorResponse struct { - status int - message string -} - -// custom error type for detecting internal application errors -func (e *errorResponse) Error() string { - return e.message -} diff --git a/www/routes_test.go b/www/routes_test.go deleted file mode 100644 index 58b25ff5..00000000 --- a/www/routes_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package www - -import "testing" - -func TestRoutes(t *testing.T) { - -} diff --git a/www/service.go b/www/service.go deleted file mode 100644 index d1173b90..00000000 --- a/www/service.go +++ /dev/null @@ -1,61 +0,0 @@ -package www - -import ( - "log" - "net/http" - - "github.com/go-chi/chi/middleware" - "github.com/go-chi/chi/v5" - - h "github.com/hyphengolang/prelude/http" - - "github.com/rog-golang-buddies/rapidmidiex/internal/suid" - ws "github.com/rog-golang-buddies/rapidmidiex/www/websocket" -) - -type Service struct { - r chi.Router - l *log.Logger - - c *ws.Client -} - -func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } - -func NewService(r chi.Router) *Service { - s := &Service{r, log.Default(), ws.DefaultClient} - s.routes() - return s -} - -func (s Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { - h.Respond(w, r, data, status) -} - -func (s Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { - return h.Decode(w, r, data) -} - -func (s Service) fileServer(prefix string, dirname string) http.Handler { - return h.FileServer(prefix, dirname) -} - -func (S Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { - return suid.ParseString(chi.URLParam(r, "id")) -} - -func (s *Service) routes() { - // middleware - s.r.Use(middleware.Logger) - - // temporary static files - s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) - s.r.Get("/", s.indexHTML("ui/www/index.html")) - s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) - - // v1 - s.r.Get("/api/v1/jam", s.handleListRooms()) - s.r.Post("/api/v1/jam", s.handleCreateRoom()) - s.r.Get("/api/v1/jam/{id}", s.handleGetRoom()) - s.r.Get("/api/v1/jam/{id}/ws", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.wsConnectionPool)) -} From f68d41c3b6d2b5de92b66b234639d167f56095c8 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 22 Sep 2022 07:42:34 +0100 Subject: [PATCH 061/246] go mod tidy --- go.mod | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/go.mod b/go.mod index a75d09f3..7f41be73 100644 --- a/go.mod +++ b/go.mod @@ -12,21 +12,36 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/hyphengolang/prelude v0.0.7 github.com/rs/cors v1.8.2 + github.com/spf13/viper v1.13.0 golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 ) require ( github.com/containerd/console v1.0.3 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.12.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.4.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( From f39ef19cb9c3595ebb9cc888a602183eb58f8ffd Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 23 Sep 2022 18:08:48 +0100 Subject: [PATCH 062/246] move "api" package to "service" package --- api/routes_test.go | 7 ------ cmd/env_test.go | 11 ++++++--- cmd/main.go | 36 ++++++++++++++++++++++------ load_dotenv.sh | 5 ++++ {api => service}/api.go | 2 +- {api => service}/errors.go | 2 +- {api => service}/middleware.go | 4 ++-- {api => service}/routes.go | 16 ++++++++++--- service/routes_test.go | 24 +++++++++++++++++++ {api => service}/service.go | 9 ++++--- {api => service}/views.go | 3 ++- {api => service}/websocket/client.go | 0 {api => service}/websocket/conn.go | 0 {api => service}/websocket/errors.go | 0 {api => service}/websocket/pool.go | 0 15 files changed, 91 insertions(+), 28 deletions(-) delete mode 100644 api/routes_test.go create mode 100644 load_dotenv.sh rename {api => service}/api.go (97%) rename {api => service}/errors.go (92%) rename {api => service}/middleware.go (93%) rename {api => service}/routes.go (85%) create mode 100644 service/routes_test.go rename {api => service}/service.go (90%) rename {api => service}/views.go (97%) rename {api => service}/websocket/client.go (100%) rename {api => service}/websocket/conn.go (100%) rename {api => service}/websocket/errors.go (100%) rename {api => service}/websocket/pool.go (100%) diff --git a/api/routes_test.go b/api/routes_test.go deleted file mode 100644 index 7f90bc7d..00000000 --- a/api/routes_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package api - -import "testing" - -func TestRoutes(t *testing.T) { - -} diff --git a/cmd/env_test.go b/cmd/env_test.go index bf433afe..bce52321 100644 --- a/cmd/env_test.go +++ b/cmd/env_test.go @@ -2,10 +2,15 @@ package main import ( "testing" - - "github.com/spf13/viper" ) func TestEnv(t *testing.T) { - viper.SetConfigFile(".env") + // backend server address/port flag + // frontend address/port flag + // timeout - read, write, idle + // + + if err := loadConfig(); err != nil { + t.Fatal(err) + } } diff --git a/cmd/main.go b/cmd/main.go index 7e466684..364f8c8c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,19 +2,23 @@ package main import ( "context" + "flag" "log" "net" "net/http" "os" "os/signal" + "path/filepath" + "runtime" "syscall" "time" "github.com/go-chi/chi/v5" "github.com/rs/cors" + "github.com/spf13/viper" "golang.org/x/sync/errgroup" - "github.com/rog-golang-buddies/rapidmidiex/api" + "github.com/rog-golang-buddies/rapidmidiex/service" ) func main() { @@ -39,12 +43,15 @@ func run() error { } srv := http.Server{ - Addr: ":" + port, - Handler: cors.New(c).Handler(api.NewService(chi.NewMux())), - ReadTimeout: 10 * time.Second, // max time to read request from the client - WriteTimeout: 10 * time.Second, // max time to write response to the client - IdleTimeout: 120 * time.Second, // max time for connections using TCP Keep-Alive - BaseContext: func(_ net.Listener) context.Context { return sCtx }, + Addr: ":" + port, + Handler: cors.New(c).Handler(service.New(chi.NewMux())), + // max time to read request from the client + ReadTimeout: 10 * time.Second, + // max time to write response to the client + WriteTimeout: 10 * time.Second, + // max time for connections using TCP Keep-Alive + IdleTimeout: 120 * time.Second, + BaseContext: func(_ net.Listener) context.Context { return sCtx }, } g, gCtx := errgroup.WithContext(sCtx) @@ -109,3 +116,18 @@ func init() { // err = viper.Unmarshal(&config) // return // } + +func loadConfig() error { + _, b, _, _ := runtime.Caller(0) + basepath := filepath.Join(filepath.Dir(b), "../") + viper.SetConfigFile(basepath + ".env") + // viper.AddConfigPath("../") + viper.SetConfigType("dotenv") + // viper.SetConfigFile(".env") + + return viper.ReadInConfig() +} + +var ( + port = flag.Int("SERVER_PORT", 8888, "The port that the server will be running on") +) diff --git a/load_dotenv.sh b/load_dotenv.sh new file mode 100644 index 00000000..27e2be25 --- /dev/null +++ b/load_dotenv.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +if [ ! -f .env ]; then + export "cmd .env | .." +fi diff --git a/api/api.go b/service/api.go similarity index 97% rename from api/api.go rename to service/api.go index b0f4baa8..d3690a29 100644 --- a/api/api.go +++ b/service/api.go @@ -1,4 +1,4 @@ -package api +package service import ( "net/http" diff --git a/api/errors.go b/service/errors.go similarity index 92% rename from api/errors.go rename to service/errors.go index d8ad7b83..fc4831ce 100644 --- a/api/errors.go +++ b/service/errors.go @@ -1,4 +1,4 @@ -package api +package service import ( "errors" diff --git a/api/middleware.go b/service/middleware.go similarity index 93% rename from api/middleware.go rename to service/middleware.go index 04b4a937..59dcf1c1 100644 --- a/api/middleware.go +++ b/service/middleware.go @@ -1,4 +1,4 @@ -package api +package service import ( "context" @@ -6,7 +6,7 @@ import ( "github.com/gorilla/websocket" - ws "github.com/rog-golang-buddies/rapidmidiex/api/websocket" + ws "github.com/rog-golang-buddies/rapidmidiex/service/websocket" ) func (s Service) connectionPool(f http.HandlerFunc) http.HandlerFunc { diff --git a/api/routes.go b/service/routes.go similarity index 85% rename from api/routes.go rename to service/routes.go index a72ec3f7..1eea8d6a 100644 --- a/api/routes.go +++ b/service/routes.go @@ -1,12 +1,12 @@ -package api +package service import ( "encoding/json" "net/http" - ws "github.com/rog-golang-buddies/rapidmidiex/api/websocket" rmx "github.com/rog-golang-buddies/rapidmidiex/internal" "github.com/rog-golang-buddies/rapidmidiex/internal/suid" + ws "github.com/rog-golang-buddies/rapidmidiex/service/websocket" ) func (s Service) handleP2PComms() http.HandlerFunc { @@ -111,9 +111,19 @@ func (s Service) handleListRooms() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { v := &response{ - Sessions: rmx.FMap(s.c.List(), func(p *ws.Pool) Session { return Session{ID: p.ID.ShortUUID(), UserCount: p.Size()} }), + Sessions: rmx.FMap(s.c.List(), func(p *ws.Pool) Session { + return Session{ + ID: p.ID.ShortUUID(), + Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), + UserCount: p.Size(), + } + }), } s.respond(w, r, v, http.StatusOK) } } + +func (s Service) handlePing(w http.ResponseWriter, r *http.Request) { + s.respond(w, r, nil, http.StatusNoContent) +} diff --git a/service/routes_test.go b/service/routes_test.go new file mode 100644 index 00000000..71dfc18d --- /dev/null +++ b/service/routes_test.go @@ -0,0 +1,24 @@ +package service + +import ( + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" +) + +func TestRoutes(t *testing.T) { + srv := New(chi.NewMux()) + + s := httptest.NewServer(srv) + t.Cleanup(func() { s.Close() }) + + r, err := s.Client().Get(s.URL + "/ping") + if err != nil { + t.Fatal(err) + } + + if r.StatusCode != 204 { + t.Fatalf("expected %d;got %d", 204, r.StatusCode) + } +} diff --git a/api/service.go b/service/service.go similarity index 90% rename from api/service.go rename to service/service.go index 25b6a277..baa73af9 100644 --- a/api/service.go +++ b/service/service.go @@ -1,4 +1,4 @@ -package api +package service import ( "log" @@ -9,8 +9,8 @@ import ( h "github.com/hyphengolang/prelude/http" - ws "github.com/rog-golang-buddies/rapidmidiex/api/websocket" "github.com/rog-golang-buddies/rapidmidiex/internal/suid" + ws "github.com/rog-golang-buddies/rapidmidiex/service/websocket" ) type Service struct { @@ -22,7 +22,7 @@ type Service struct { func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } -func NewService(r chi.Router) *Service { +func New(r chi.Router) *Service { s := &Service{r, log.Default(), ws.DefaultClient} s.routes() return s @@ -48,6 +48,9 @@ func (s *Service) routes() { // middleware s.r.Use(middleware.Logger) + // ping + s.r.Get("/ping", s.handlePing) + // temporary static files s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) s.r.Get("/", s.indexHTML("ui/www/index.html")) diff --git a/api/views.go b/service/views.go similarity index 97% rename from api/views.go rename to service/views.go index eade241d..e4cbfdaf 100644 --- a/api/views.go +++ b/service/views.go @@ -1,7 +1,8 @@ -package api +package service import "net/http" import t "github.com/hyphengolang/prelude/template" + // ! to be discarded func (s *Service) indexHTML(path string) http.HandlerFunc { diff --git a/api/websocket/client.go b/service/websocket/client.go similarity index 100% rename from api/websocket/client.go rename to service/websocket/client.go diff --git a/api/websocket/conn.go b/service/websocket/conn.go similarity index 100% rename from api/websocket/conn.go rename to service/websocket/conn.go diff --git a/api/websocket/errors.go b/service/websocket/errors.go similarity index 100% rename from api/websocket/errors.go rename to service/websocket/errors.go diff --git a/api/websocket/pool.go b/service/websocket/pool.go similarity index 100% rename from api/websocket/pool.go rename to service/websocket/pool.go From cfe80c152bb84a9360ea302e463764e60cb5ce7a Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 23 Sep 2022 18:13:55 +0100 Subject: [PATCH 063/246] move `websocket` package inside `internal` --- {service => internal}/websocket/client.go | 0 {service => internal}/websocket/conn.go | 0 {service => internal}/websocket/errors.go | 0 {service => internal}/websocket/pool.go | 0 service/middleware.go | 2 +- service/routes.go | 2 +- service/service.go | 2 +- 7 files changed, 3 insertions(+), 3 deletions(-) rename {service => internal}/websocket/client.go (100%) rename {service => internal}/websocket/conn.go (100%) rename {service => internal}/websocket/errors.go (100%) rename {service => internal}/websocket/pool.go (100%) diff --git a/service/websocket/client.go b/internal/websocket/client.go similarity index 100% rename from service/websocket/client.go rename to internal/websocket/client.go diff --git a/service/websocket/conn.go b/internal/websocket/conn.go similarity index 100% rename from service/websocket/conn.go rename to internal/websocket/conn.go diff --git a/service/websocket/errors.go b/internal/websocket/errors.go similarity index 100% rename from service/websocket/errors.go rename to internal/websocket/errors.go diff --git a/service/websocket/pool.go b/internal/websocket/pool.go similarity index 100% rename from service/websocket/pool.go rename to internal/websocket/pool.go diff --git a/service/middleware.go b/service/middleware.go index 59dcf1c1..10997517 100644 --- a/service/middleware.go +++ b/service/middleware.go @@ -6,7 +6,7 @@ import ( "github.com/gorilla/websocket" - ws "github.com/rog-golang-buddies/rapidmidiex/service/websocket" + ws "github.com/rog-golang-buddies/rapidmidiex/internal/websocket" ) func (s Service) connectionPool(f http.HandlerFunc) http.HandlerFunc { diff --git a/service/routes.go b/service/routes.go index 1eea8d6a..a01ef3e8 100644 --- a/service/routes.go +++ b/service/routes.go @@ -6,7 +6,7 @@ import ( rmx "github.com/rog-golang-buddies/rapidmidiex/internal" "github.com/rog-golang-buddies/rapidmidiex/internal/suid" - ws "github.com/rog-golang-buddies/rapidmidiex/service/websocket" + ws "github.com/rog-golang-buddies/rapidmidiex/internal/websocket" ) func (s Service) handleP2PComms() http.HandlerFunc { diff --git a/service/service.go b/service/service.go index baa73af9..683e011e 100644 --- a/service/service.go +++ b/service/service.go @@ -10,7 +10,7 @@ import ( h "github.com/hyphengolang/prelude/http" "github.com/rog-golang-buddies/rapidmidiex/internal/suid" - ws "github.com/rog-golang-buddies/rapidmidiex/service/websocket" + ws "github.com/rog-golang-buddies/rapidmidiex/internal/websocket" ) type Service struct { From beeb6884d93ca168bd128754707e45deb6f6b93a Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 23 Sep 2022 18:17:36 +0100 Subject: [PATCH 064/246] `ap`i &`middleware` to `service` & `websocket` --- service/api.go | 32 ------------ service/middleware.go | 56 -------------------- service/routes.go | 58 --------------------- service/service.go | 23 +++++++++ service/websocket.go | 116 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 146 deletions(-) delete mode 100644 service/api.go delete mode 100644 service/middleware.go create mode 100644 service/websocket.go diff --git a/service/api.go b/service/api.go deleted file mode 100644 index d3690a29..00000000 --- a/service/api.go +++ /dev/null @@ -1,32 +0,0 @@ -package service - -import ( - "net/http" - - h "github.com/hyphengolang/prelude/http" - - "github.com/rog-golang-buddies/rapidmidiex/internal/suid" -) - -type contextKey string - -var ( - roomKey = contextKey("rmx-fetch-pool") - upgradeKey = contextKey("rmx-upgrade-http") -) - -func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } - -type Session struct { - ID suid.SUID `json:"id"` - Name string `json:"name,omitempty"` - Users []suid.SUID `json:"users,omitempty"` - /* Not really required */ - UserCount int `json:"userCount"` -} - -type User struct { - ID suid.SUID `json:"id"` - Name string `json:"name,omitempty"` - /* More fields can belong here */ -} diff --git a/service/middleware.go b/service/middleware.go deleted file mode 100644 index 10997517..00000000 --- a/service/middleware.go +++ /dev/null @@ -1,56 +0,0 @@ -package service - -import ( - "context" - "net/http" - - "github.com/gorilla/websocket" - - ws "github.com/rog-golang-buddies/rapidmidiex/internal/websocket" -) - -func (s Service) connectionPool(f http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r) - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - p, err := s.c.Get(uid) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - r = r.WithContext(context.WithValue(r.Context(), roomKey, p)) - f(w, r) - } -} - -func (s Service) upgradeHTTP(readBuf, writeBuf int) func(f http.HandlerFunc) http.HandlerFunc { - return func(f http.HandlerFunc) http.HandlerFunc { - u := &websocket.Upgrader{ - ReadBufferSize: readBuf, - WriteBufferSize: writeBuf, - CheckOrigin: func(r *http.Request) bool { return true }, - } - - return func(w http.ResponseWriter, r *http.Request) { - p := r.Context().Value(roomKey).(*ws.Pool) - if p.Size() == p.MaxConn { - s.respond(w, r, ws.ErrMaxConn, http.StatusUnauthorized) - return - } - - c, err := p.NewConn(w, r, u) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - r = r.WithContext(context.WithValue(r.Context(), upgradeKey, c)) - f(w, r) - } - } -} diff --git a/service/routes.go b/service/routes.go index a01ef3e8..a9ea9c16 100644 --- a/service/routes.go +++ b/service/routes.go @@ -1,7 +1,6 @@ package service import ( - "encoding/json" "net/http" rmx "github.com/rog-golang-buddies/rapidmidiex/internal" @@ -9,63 +8,6 @@ import ( ws "github.com/rog-golang-buddies/rapidmidiex/internal/websocket" ) -func (s Service) handleP2PComms() http.HandlerFunc { - type response[T any] struct { - Typ rmx.MessageTyp `json:"type"` - Payload T `json:"payload"` - } - - type join struct { - ID suid.SUID `json:"id"` - SessionID suid.SUID `json:"sessionId"` - } - - type leave struct { - ID suid.SUID `json:"id"` - SessionID suid.SUID `json:"sessionId"` - Error any `json:"err"` - } - - return func(w http.ResponseWriter, r *http.Request) { - c := r.Context().Value(upgradeKey).(*ws.Conn) - defer func() { - // ! send error when Leaving session pool - c.SendMessage(response[leave]{ - Typ: rmx.Leave, - Payload: leave{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, - }) - - c.Close() - }() - - if err := c.SendMessage(response[join]{ - Typ: rmx.Join, - Payload: join{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, - }); err != nil { - s.l.Println(err) - return - } - - // ? could the API be adjusted such that - // ? this for-loop only needs to read and - // ? never touch the code for writing - for { - var msg response[json.RawMessage] - if err := c.ReadJSON(&msg); err != nil { - s.l.Println(err) - return - } - - // * here the message will be passed off to a different handler - // * via a go routine* - if err := c.SendMessage(response[int]{Typ: rmx.Message, Payload: 10}); err != nil { - s.l.Println(err) - return - } - } - } -} - func (s Service) handleCreateRoom() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, err := s.c.NewPool() diff --git a/service/service.go b/service/service.go index 683e011e..8b5951c3 100644 --- a/service/service.go +++ b/service/service.go @@ -13,6 +13,29 @@ import ( ws "github.com/rog-golang-buddies/rapidmidiex/internal/websocket" ) +type contextKey string + +var ( + roomKey = contextKey("rmx-fetch-pool") + upgradeKey = contextKey("rmx-upgrade-http") +) + +func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } + +type Session struct { + ID suid.SUID `json:"id"` + Name string `json:"name,omitempty"` + Users []suid.SUID `json:"users,omitempty"` + /* Not really required */ + UserCount int `json:"userCount"` +} + +type User struct { + ID suid.SUID `json:"id"` + Name string `json:"name,omitempty"` + /* More fields can belong here */ +} + type Service struct { r chi.Router l *log.Logger diff --git a/service/websocket.go b/service/websocket.go new file mode 100644 index 00000000..8369f5af --- /dev/null +++ b/service/websocket.go @@ -0,0 +1,116 @@ +package service + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/gorilla/websocket" + + rmx "github.com/rog-golang-buddies/rapidmidiex/internal" + "github.com/rog-golang-buddies/rapidmidiex/internal/suid" + ws "github.com/rog-golang-buddies/rapidmidiex/internal/websocket" +) + +func (s Service) handleP2PComms() http.HandlerFunc { + type response[T any] struct { + Typ rmx.MessageTyp `json:"type"` + Payload T `json:"payload"` + } + + type join struct { + ID suid.SUID `json:"id"` + SessionID suid.SUID `json:"sessionId"` + } + + type leave struct { + ID suid.SUID `json:"id"` + SessionID suid.SUID `json:"sessionId"` + Error any `json:"err"` + } + + return func(w http.ResponseWriter, r *http.Request) { + c := r.Context().Value(upgradeKey).(*ws.Conn) + defer func() { + // ! send error when Leaving session pool + c.SendMessage(response[leave]{ + Typ: rmx.Leave, + Payload: leave{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, + }) + + c.Close() + }() + + if err := c.SendMessage(response[join]{ + Typ: rmx.Join, + Payload: join{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, + }); err != nil { + s.l.Println(err) + return + } + + // ? could the API be adjusted such that + // ? this for-loop only needs to read and + // ? never touch the code for writing + for { + var msg response[json.RawMessage] + if err := c.ReadJSON(&msg); err != nil { + s.l.Println(err) + return + } + + // * here the message will be passed off to a different handler + // * via a go routine* + if err := c.SendMessage(response[int]{Typ: rmx.Message, Payload: 10}); err != nil { + s.l.Println(err) + return + } + } + } +} + +func (s Service) connectionPool(f http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + uid, err := s.parseUUID(w, r) + if err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + p, err := s.c.Get(uid) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + r = r.WithContext(context.WithValue(r.Context(), roomKey, p)) + f(w, r) + } +} + +func (s Service) upgradeHTTP(readBuf, writeBuf int) func(f http.HandlerFunc) http.HandlerFunc { + return func(f http.HandlerFunc) http.HandlerFunc { + u := &websocket.Upgrader{ + ReadBufferSize: readBuf, + WriteBufferSize: writeBuf, + CheckOrigin: func(r *http.Request) bool { return true }, + } + + return func(w http.ResponseWriter, r *http.Request) { + p := r.Context().Value(roomKey).(*ws.Pool) + if p.Size() == p.MaxConn { + s.respond(w, r, ws.ErrMaxConn, http.StatusUnauthorized) + return + } + + c, err := p.NewConn(w, r, u) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + r = r.WithContext(context.WithValue(r.Context(), upgradeKey, c)) + f(w, r) + } + } +} From b541414dfdc557f764810d68610ebbb2c53f56c3 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 23 Sep 2022 19:20:42 +0100 Subject: [PATCH 065/246] soft-delete of internal web ui --- service/service.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/service/service.go b/service/service.go index 8b5951c3..da77113f 100644 --- a/service/service.go +++ b/service/service.go @@ -55,13 +55,9 @@ func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, stat h.Respond(w, r, data, status) } -func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { - return h.Decode(w, r, data) -} - -func (s *Service) fileServer(prefix string, dirname string) http.Handler { - return h.FileServer(prefix, dirname) -} +// ! deprecated +// func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { return h.Decode(w, r, data) } +// func (s *Service) fileServer(prefix string, dirname string) http.Handler { return h.FileServer(prefix, dirname) } func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { return suid.ParseString(chi.URLParam(r, "id")) @@ -75,9 +71,9 @@ func (s *Service) routes() { s.r.Get("/ping", s.handlePing) // temporary static files - s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) - s.r.Get("/", s.indexHTML("ui/www/index.html")) - s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) + // s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) + // s.r.Get("/", s.indexHTML("ui/www/index.html")) + // s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) // REST v1 s.r.Get("/api/v1/jam", s.handleListRooms()) From 303191ffeada0105d41c8069de30bede6518b797 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 23 Sep 2022 20:09:26 +0100 Subject: [PATCH 066/246] clean up --- cmd/main.go | 19 ++++++++++--------- internal/websocket/pool.go | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 364f8c8c..42125e73 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -21,6 +21,10 @@ import ( "github.com/rog-golang-buddies/rapidmidiex/service" ) +var ( + port = flag.Int("SERVER_PORT", 8888, "The port that the server will be running on") +) + func main() { if err := run(); err != nil { log.Fatalln(err) @@ -52,13 +56,14 @@ func run() error { // max time for connections using TCP Keep-Alive IdleTimeout: 120 * time.Second, BaseContext: func(_ net.Listener) context.Context { return sCtx }, + ErrorLog: log.Default(), } g, gCtx := errgroup.WithContext(sCtx) g.Go(func() error { // Run the server - log.Printf("App server starting on %s", srv.Addr) + srv.ErrorLog.Printf("App server starting on %s", srv.Addr) return srv.ListenAndServe() }) @@ -67,11 +72,11 @@ func run() error { return srv.Shutdown(context.Background()) }) - if err := g.Wait(); err != nil { - log.Printf("exit reason: %s \n", err) - } + // if err := g.Wait(); err != nil { + // log.Printf("exit reason: %s \n", err) + // } - return nil + return g.Wait() } func getEnv(key, fallback string) string { @@ -127,7 +132,3 @@ func loadConfig() error { return viper.ReadInConfig() } - -var ( - port = flag.Int("SERVER_PORT", 8888, "The port that the server will be running on") -) diff --git a/internal/websocket/pool.go b/internal/websocket/pool.go index 85ee5da7..fd67fc73 100644 --- a/internal/websocket/pool.go +++ b/internal/websocket/pool.go @@ -95,3 +95,19 @@ func (p *Pool) Close() error { return nil } + +/* +Experimental: Pub/Sub pattern + +func (p *Pool) AddEventListener(eventTyp string, callback func(event any)) { + if eventTyp already exists then panic + if callback is nil then panic + + type entry struct { eventTyp string; callback func(event any); } + add entry to to map[eventType]entry +} + +func (p *Pool) Listen() { + +} +*/ From 8be0c6813d67d3b4a9d5a02de001dfeb32bd9a59 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Fri, 23 Sep 2022 23:20:59 -0400 Subject: [PATCH 067/246] Creating a new Jam Session --- .vscode/launch.json | 10 ++++++ ui/terminal/tui/jamui/jam.go | 45 ++++++++++++++++++--------- ui/terminal/tui/lobbyui/lobby.go | 53 ++++++++++++++++++++++++++------ ui/terminal/tui/tui.go | 8 +++-- 4 files changed, 89 insertions(+), 27 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 9386cbc2..cff0396b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,16 @@ "program": "${workspaceFolder}/cmd", "cwd": "${workspaceFolder}", "envFile": "${workspaceFolder}/cmd/.env" + }, + { + "name": "TUI Client", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/ui/terminal", + "cwd": "${workspaceFolder}", + "args": ["--server", "http://localhost:9003"], + "console": "integratedTerminal" } ] } diff --git a/ui/terminal/tui/jamui/jam.go b/ui/terminal/tui/jamui/jam.go index c921b9ea..72bff3b8 100644 --- a/ui/terminal/tui/jamui/jam.go +++ b/ui/terminal/tui/jamui/jam.go @@ -7,6 +7,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/gorilla/websocket" "golang.org/x/term" ) @@ -46,6 +47,9 @@ var ( Padding(0, 1) ) +// Message Types +type Entered struct{} + type pianoKey struct { noteNumber int // MIDI note number ie: 72 name string // Name of musical note, ie: "C5" @@ -55,6 +59,25 @@ type pianoKey struct { type Model struct { piano []pianoKey // Piano keys. {"q": pianoKey{72, "C5", "q", ...}} activeKeys map[string]struct{} // Currently active piano keys + Socket *websocket.Conn // Websocket connection for current Jam Session + ID string // Jam Session ID +} + +func New() Model { + return Model{ + piano: []pianoKey{ + {72, "C5", "q"}, + {74, "D5", "w"}, + {76, "E5", "e"}, + {77, "F5", "r"}, + {79, "G5", "t"}, + {81, "A5", "y"}, + {83, "B5", "u"}, + {84, "C6", "i"}, + }, + + activeKeys: make(map[string]struct{}), + } } func (m Model) Init() tea.Cmd { @@ -74,7 +97,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { fmt.Printf("Key press: %s\n", msg.String()) } + // Entered the Jam Session + case Entered: + fmt.Println(m) } + return m, nil } @@ -101,19 +128,7 @@ func (m Model) View() string { return docStyle.Render(doc.String()) } -func New() tea.Model { - return Model{ - piano: []pianoKey{ - {72, "C5", "q"}, - {74, "D5", "w"}, - {76, "E5", "e"}, - {77, "F5", "r"}, - {79, "G5", "t"}, - {81, "A5", "y"}, - {83, "B5", "u"}, - {84, "C6", "i"}, - }, - - activeKeys: make(map[string]struct{}), - } +// Commands +func Enter() tea.Msg { + return Entered{} } diff --git a/ui/terminal/tui/lobbyui/lobby.go b/ui/terminal/tui/lobbyui/lobby.go index f6a4b708..771e75cb 100644 --- a/ui/terminal/tui/lobbyui/lobby.go +++ b/ui/terminal/tui/lobbyui/lobby.go @@ -65,6 +65,11 @@ type jamsResp struct { Sessions []Session `json:"sessions"` } +type jamCreated struct { + ID string `json:"id"` + UserCount int `json:"userCount"` +} + // For messages that contain errors it's often handy to also implement the // error interface on the message. func (e errMsg) Error() string { return e.err.Error() } @@ -94,7 +99,8 @@ func FetchSessions(baseURL string) tea.Cmd { } type Model struct { - wsURL string + wsURL string // Websocket endpoint + apiURL string // REST API base endpoint sessions []Session jamTable table.Model help tea.Model @@ -102,9 +108,10 @@ type Model struct { err error } -func New(wsURL string) tea.Model { +func New(wsURL, apiURL string) tea.Model { return Model{ wsURL: wsURL, + apiURL: apiURL, help: NewHelpModel(), loading: true, } @@ -120,20 +127,27 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.jamTable.SetWidth(msg.Width - 10) + case errMsg: + // There was an error. Note it in the model. + m.err = msg case jamsResp: m.sessions = msg.Sessions m.jamTable = makeJamsTable(m) m.jamTable.Focus() m.loading = false - case errMsg: - // There was an error. Note it in the model. - m.err = msg + case jamCreated: + jamID := msg.ID + // Auto join the newly created Jam + cmds = append(cmds, jamConnect(m.wsURL, jamID)) case tea.KeyMsg: switch msg.String() { case tea.KeyEnter.String(): - jamId := m.jamTable.SelectedRow()[1] + jamID := m.jamTable.SelectedRow()[1] - cmds = append(cmds, jamConnect(m.wsURL, jamId)) + cmds = append(cmds, jamConnect(m.wsURL, jamID)) + case "n": + // Create new Jam Session + cmds = append(cmds, jamCreate(m.apiURL)) } } newJamTable, jtCmd := m.jamTable.Update(msg) @@ -238,9 +252,11 @@ func makeJamsTable(m Model) table.Model { } type JamConnected struct { - WS *websocket.Conn + WS *websocket.Conn + JamID string } +// Commands func jamConnect(baseURL, jamID string) tea.Cmd { return func() tea.Msg { url := baseURL + "/" + jamID @@ -251,7 +267,26 @@ func jamConnect(baseURL, jamID string) tea.Cmd { } // TODO: Actually connect to Jam Session over websocket return JamConnected{ - WS: ws, + WS: ws, + JamID: jamID, + } + } +} + +func jamCreate(baseURL string) tea.Cmd { + // For now, we're just creating the Jam Session without + // and options. + // Next step would be to show inputs for Jam details + // (name, bpm, etc) before creating the Jam. + return func() tea.Msg { + resp, err := http.Post(baseURL+"/jam", "application/json", strings.NewReader("{}")) + if err != nil { + return errMsg{err: fmt.Errorf("jamCreate: %v", err)} } + var body jamCreated + decoder := json.NewDecoder(resp.Body) + decoder.Decode(&body) + + return body } } diff --git a/ui/terminal/tui/tui.go b/ui/terminal/tui/tui.go index 561d2760..cc93a8a0 100644 --- a/ui/terminal/tui/tui.go +++ b/ui/terminal/tui/tui.go @@ -33,7 +33,7 @@ const ( type mainModel struct { curView appView lobby tea.Model - jam tea.Model + jam jamui.Model RESTendpoint string WSendpoint string jamSocket *websocket.Conn // Websocket connection to a Jam Session @@ -48,7 +48,7 @@ func NewModel(serverHostURL string) (mainModel, error) { return mainModel{ curView: lobbyView, - lobby: lobbyui.New(wsURL.String() + "/ws"), + lobby: lobbyui.New(wsURL.String()+"/ws", serverHostURL+"/api/v1"), jam: jamui.New(), RESTendpoint: serverHostURL + "/api/v1", }, nil @@ -73,7 +73,9 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case lobbyui.JamConnected: m.curView = jamView - m.jamSocket = msg.WS + m.jam.Socket = msg.WS + m.jam.ID = msg.JamID + cmds = append(cmds, jamui.Enter) } // Call sub-model Updates From 2b11200f4f235a32139927db7b162bd7b9cb6242 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 24 Sep 2022 11:18:15 +0100 Subject: [PATCH 068/246] adding testable endpoint --- service/routes.go | 2 +- service/routes_test.go | 3 ++ service/service.go | 9 ++-- service/websocket.go | 109 +++++++++++++++++++++++++++++------------ 4 files changed, 87 insertions(+), 36 deletions(-) diff --git a/service/routes.go b/service/routes.go index a9ea9c16..0734f023 100644 --- a/service/routes.go +++ b/service/routes.go @@ -10,7 +10,7 @@ import ( func (s Service) handleCreateRoom() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.c.NewPool() + uid, err := s.c.NewPool(4) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) return diff --git a/service/routes_test.go b/service/routes_test.go index 71dfc18d..7116ebd5 100644 --- a/service/routes_test.go +++ b/service/routes_test.go @@ -5,11 +5,14 @@ import ( "testing" "github.com/go-chi/chi/v5" + // "github.com/rog-golang-buddies/rapidmidiex/internal/websocket" ) func TestRoutes(t *testing.T) { srv := New(chi.NewMux()) + // srv.r.Get("/ws/echo", chain(srv.handleEcho(),srv.upgradeHTTP(1024,1024),srv.connectionPool(websocket.DefaultPool()))) + s := httptest.NewServer(srv) t.Cleanup(func() { s.Close() }) diff --git a/service/service.go b/service/service.go index da77113f..76b71c89 100644 --- a/service/service.go +++ b/service/service.go @@ -67,9 +67,6 @@ func (s *Service) routes() { // middleware s.r.Use(middleware.Logger) - // ping - s.r.Get("/ping", s.handlePing) - // temporary static files // s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) // s.r.Get("/", s.indexHTML("ui/www/index.html")) @@ -80,6 +77,10 @@ func (s *Service) routes() { s.r.Post("/api/v1/jam", s.handleCreateRoom()) s.r.Get("/api/v1/jam/{id}", s.handleGetRoom()) + s.r.Get("/ping", s.handlePing) + // Websocket - s.r.Get("/ws/{id}", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.connectionPool)) + s.r.Get("/ws/jam/{id}", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.connectionPool(nil))) + + s.r.Get("/ws/ping", chain(s.handleEcho(), s.upgradeHTTP(1024, 1024), s.connectionPool(ws.DefaultPool()))) } diff --git a/service/websocket.go b/service/websocket.go index 8369f5af..54a474b1 100644 --- a/service/websocket.go +++ b/service/websocket.go @@ -12,6 +12,65 @@ import ( ws "github.com/rog-golang-buddies/rapidmidiex/internal/websocket" ) +func (s Service) connectionPool(p *ws.Pool) func(f http.HandlerFunc) http.HandlerFunc { + // * testing/debugging + if p != nil { + return func(f http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + s.l.Println("default for ping") + r = r.WithContext(context.WithValue(r.Context(), roomKey, p)) + f(w, r) + } + } + } + + return func(f http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + uid, err := s.parseUUID(w, r) + if err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + p, err := s.c.Get(uid) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + r = r.WithContext(context.WithValue(r.Context(), roomKey, p)) + f(w, r) + } + } +} + +func (s Service) upgradeHTTP(readBuf, writeBuf int) func(f http.HandlerFunc) http.HandlerFunc { + return func(f http.HandlerFunc) http.HandlerFunc { + u := &websocket.Upgrader{ + ReadBufferSize: readBuf, + WriteBufferSize: writeBuf, + CheckOrigin: func(r *http.Request) bool { return true }, + } + + return func(w http.ResponseWriter, r *http.Request) { + p := r.Context().Value(roomKey).(*ws.Pool) + if p.Size() == p.MaxConn { + s.respond(w, r, ws.ErrMaxConn, http.StatusUnauthorized) + return + } + + c, err := p.NewConn(w, r, u) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + r = r.WithContext(context.WithValue(r.Context(), upgradeKey, c)) + f(w, r) + } + } +} + func (s Service) handleP2PComms() http.HandlerFunc { type response[T any] struct { Typ rmx.MessageTyp `json:"type"` @@ -69,48 +128,36 @@ func (s Service) handleP2PComms() http.HandlerFunc { } } -func (s Service) connectionPool(f http.HandlerFunc) http.HandlerFunc { +func (s Service) handleEcho() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r) - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } + c := r.Context().Value(upgradeKey).(*ws.Conn) + defer func() { + // ! send error when Leaving session pool + c.SendMessage("leave") - p, err := s.c.Get(uid) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } + s.l.Println("default leave") - r = r.WithContext(context.WithValue(r.Context(), roomKey, p)) - f(w, r) - } -} + c.Close() + }() -func (s Service) upgradeHTTP(readBuf, writeBuf int) func(f http.HandlerFunc) http.HandlerFunc { - return func(f http.HandlerFunc) http.HandlerFunc { - u := &websocket.Upgrader{ - ReadBufferSize: readBuf, - WriteBufferSize: writeBuf, - CheckOrigin: func(r *http.Request) bool { return true }, + if err := c.SendMessage("join"); err != nil { + s.l.Println(err) + return } - return func(w http.ResponseWriter, r *http.Request) { - p := r.Context().Value(roomKey).(*ws.Pool) - if p.Size() == p.MaxConn { - s.respond(w, r, ws.ErrMaxConn, http.StatusUnauthorized) + s.l.Println("default join") + + for { + var msg any + if err := c.ReadJSON(&msg); err != nil { + s.l.Println(err) return } - c, err := p.NewConn(w, r, u) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) + if err := c.SendMessage(msg); err != nil { + s.l.Println(err) return } - - r = r.WithContext(context.WithValue(r.Context(), upgradeKey, c)) - f(w, r) } } } From 0a2dea54720098efdd1d3c35a716b614dfad1365 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 24 Sep 2022 11:18:34 +0100 Subject: [PATCH 069/246] potential mutex issue --- internal/websocket/client.go | 4 ++-- internal/websocket/pool.go | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/internal/websocket/client.go b/internal/websocket/client.go index d5487030..51244e51 100644 --- a/internal/websocket/client.go +++ b/internal/websocket/client.go @@ -32,11 +32,11 @@ func (c *Client) Close() error { return rmx.ErrTodo } -func (c *Client) NewPool() (suid.UUID, error) { +func (c *Client) NewPool(maxCount int) (suid.UUID, error) { c.mu.Lock() defer c.mu.Unlock() - p := DefaultPool() + p := NewPool(maxCount) c.ps[p.ID] = p diff --git a/internal/websocket/pool.go b/internal/websocket/pool.go index fd67fc73..8e92d054 100644 --- a/internal/websocket/pool.go +++ b/internal/websocket/pool.go @@ -22,6 +22,28 @@ type Pool struct { msgs chan any } +func NewPool(maxCount int) *Pool { + p := &Pool{ + ID: suid.NewUUID(), + MaxConn: maxCount, + cs: make(map[suid.UUID]*Conn), + msgs: make(chan any), + } + + go func() { + defer p.Close() + + for msg := range p.msgs { + for _, c := range p.cs { + c.WriteJSON(msg) + } + } + }() + + return p +} + +// maybe should just be a variable func DefaultPool() *Pool { p := &Pool{ ID: suid.NewUUID(), @@ -39,7 +61,6 @@ func DefaultPool() *Pool { } } - // ?why does this pattern not work // for _, c := range p.cs { c.WriteJSON(<-p.msgs) } }() From 0e6532587ae46bd6061a14d4508de0f8e21132ec Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 24 Sep 2022 11:21:03 +0100 Subject: [PATCH 070/246] reamove ui/web and assets folders --- assets/src/index.js | 49 ----------------------------------- assets/src/play.js | 57 ----------------------------------------- assets/src/utils.js | 16 ------------ assets/styles/index.css | 8 ------ assets/styles/play.css | 0 service/service.go | 8 +----- ui/www/404.html | 15 ----------- ui/www/index.html | 17 ------------ ui/www/play.html | 16 ------------ 9 files changed, 1 insertion(+), 185 deletions(-) delete mode 100644 assets/src/index.js delete mode 100644 assets/src/play.js delete mode 100644 assets/src/utils.js delete mode 100644 assets/styles/index.css delete mode 100644 assets/styles/play.css delete mode 100644 ui/www/404.html delete mode 100644 ui/www/index.html delete mode 100644 ui/www/play.html diff --git a/assets/src/index.js b/assets/src/index.js deleted file mode 100644 index 0e4978d8..00000000 --- a/assets/src/index.js +++ /dev/null @@ -1,49 +0,0 @@ -const app = document.getElementById('app'); - -app.innerHTML = ` -

              Welcome, Home

              -

              To create a new session tap the button below

              - - -
              -`; - -document.querySelector('button').addEventListener('click', async (e) => { - try { - const r = await fetch('/api/v1/jam', { - method: 'POST', - body: { - // TODO: Add any info from UI - }, - }); - const { sessionId } = await r.json(); - - session.id = sessionId; - } catch (e) { - console.error(e.message); - } -}); - -const session = new Proxy( - { id: '' }, - { - set(obj, prop, value) { - let v = 0; - switch (prop) { - case 'id': - obj[prop] = value; - const url = `${window.location.href}play/${value}`; - const anchor = document.createElement('a'); - anchor.href = url; - anchor.innerText = url; - anchor.target = '_blank'; - document.getElementById('session').appendChild(anchor); - - return true; - - default: - return false; - } - }, - } -); diff --git a/assets/src/play.js b/assets/src/play.js deleted file mode 100644 index 9de0c81c..00000000 --- a/assets/src/play.js +++ /dev/null @@ -1,57 +0,0 @@ -import { websocketUrl, sessionId } from './utils.js'; - -const app = document.getElementById('app'); - -app.innerHTML = ` -

              Welcome to the JAM Session

              - - -
                -`; - -const ws = new WebSocket(websocketUrl(`/api/v1/jam/${sessionId()}/ws`)); - -ws.addEventListener('open', (e) => { - document.querySelector('button').disabled = false; - - alert('web socket has opened'); -}); - -ws.addEventListener('message', async (e) => { - const { type, id } = JSON.parse(e.data); - - switch (type.toLowerCase()) { - case 'join': - newUserJoined(); - break; - case 'leave': - document.getElementById(id).remove(); - break; - } -}); - -ws.addEventListener('error', (e) => { - console.error(e); - - document.querySelector('button').disabled = true; -}); - -document.querySelector('button').addEventListener('click', (e) => { - ws.send(1); -}); - -async function newUserJoined() { - const r = await fetch(`/api/v1/jam/${sessionId()}`); - const { users } = await r.json(); - - // ^proxy Array that updates the list in the DOM - const items = []; - for (const id of users) { - const li = document.createElement('li'); - li.textContent = `${id} has joined`; - li.id = id; - items.push(li); - } - - document.querySelector(`[aria-label="users"]`).replaceChildren(...items); -} diff --git a/assets/src/utils.js b/assets/src/utils.js deleted file mode 100644 index a8f6c7c2..00000000 --- a/assets/src/utils.js +++ /dev/null @@ -1,16 +0,0 @@ -export function websocketUrl(path) { - var url = new URL(path, window.location.href); - - url.protocol = url.protocol.replace('http', 'ws'); - - return url.href; // => ws://www.example.com:9999/path/to/websocket -}; - -/** - * - * @returns {string} sessionId - */ -export const sessionId = () => { - return window.location.pathname.split("/").at(-1); -} - diff --git a/assets/styles/index.css b/assets/styles/index.css deleted file mode 100644 index 00234317..00000000 --- a/assets/styles/index.css +++ /dev/null @@ -1,8 +0,0 @@ -@import url("https://cdn.jsdelivr.net/npm/modern-normalize/modern-normalize.min.css"); - -[aria-label="users"]::before { - content: attr(aria-label); - text-transform: capitalize; - font-size: large; - font-weight: bold; -} \ No newline at end of file diff --git a/assets/styles/play.css b/assets/styles/play.css deleted file mode 100644 index e69de29b..00000000 diff --git a/service/service.go b/service/service.go index 76b71c89..58007624 100644 --- a/service/service.go +++ b/service/service.go @@ -64,14 +64,8 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, } func (s *Service) routes() { - // middleware s.r.Use(middleware.Logger) - // temporary static files - // s.r.Handle("/assets/*", s.fileServer("/assets/", "assets")) - // s.r.Get("/", s.indexHTML("ui/www/index.html")) - // s.r.Get("/play/{id}", s.jamSessionHTML("ui/www/play.html")) - // REST v1 s.r.Get("/api/v1/jam", s.handleListRooms()) s.r.Post("/api/v1/jam", s.handleCreateRoom()) @@ -82,5 +76,5 @@ func (s *Service) routes() { // Websocket s.r.Get("/ws/jam/{id}", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.connectionPool(nil))) - s.r.Get("/ws/ping", chain(s.handleEcho(), s.upgradeHTTP(1024, 1024), s.connectionPool(ws.DefaultPool()))) + s.r.Get("/ws/test", chain(s.handleEcho(), s.upgradeHTTP(1024, 1024), s.connectionPool(ws.DefaultPool()))) } diff --git a/ui/www/404.html b/ui/www/404.html deleted file mode 100644 index 4ca98714..00000000 --- a/ui/www/404.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - 404, Oh No! - - - -

                404 Error

                - - - \ No newline at end of file diff --git a/ui/www/index.html b/ui/www/index.html deleted file mode 100644 index a3a314f4..00000000 --- a/ui/www/index.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - Home - - - - - -
                - - - \ No newline at end of file diff --git a/ui/www/play.html b/ui/www/play.html deleted file mode 100644 index c34d1275..00000000 --- a/ui/www/play.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - Jam Session - - - - -
                - - - \ No newline at end of file From 58ad099531c2708601e75ef88007f7723543b20e Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 24 Sep 2022 11:34:14 +0100 Subject: [PATCH 071/246] cleanup Service.connectionPool --- service/websocket.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/service/websocket.go b/service/websocket.go index 54a474b1..676a99b5 100644 --- a/service/websocket.go +++ b/service/websocket.go @@ -13,18 +13,13 @@ import ( ) func (s Service) connectionPool(p *ws.Pool) func(f http.HandlerFunc) http.HandlerFunc { - // * testing/debugging - if p != nil { - return func(f http.HandlerFunc) http.HandlerFunc { + return func(f http.HandlerFunc) http.HandlerFunc { + if p != nil { return func(w http.ResponseWriter, r *http.Request) { - s.l.Println("default for ping") - r = r.WithContext(context.WithValue(r.Context(), roomKey, p)) - f(w, r) + f(w, r.WithContext(context.WithValue(r.Context(), roomKey, p))) } } - } - return func(f http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, err := s.parseUUID(w, r) if err != nil { From ee71fd88b4e24d39ee2af6b4a1915e98a86ed61b Mon Sep 17 00:00:00 2001 From: pmoieni Date: Sat, 24 Sep 2022 22:36:21 +0330 Subject: [PATCH 072/246] fixed makefile build command --- Makefile | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 9526e992..1cfb99d6 100644 --- a/Makefile +++ b/Makefile @@ -37,11 +37,6 @@ fmt: go fmt ./... ## build_server: Build server binary into bin/ directory -.PHONY: build_server -build_server: - $(GOFLAGS) $(GO_BUILD) -a -v -ldflags="-w -s" -o bin/server cmd/server/main.go - -## build_cli: Build cli binary into bin/ directory -.PHONY: build_cli -build_cli: - $(GOFLAGS) $(GO_BUILD) -a -v -ldflags="-w -s" -o bin/cli cmd/cli/main.go +.PHONY: build +build: + $(GOFLAGS) $(GO_BUILD) -a -v -ldflags="-w -s" -o bin/rmx-server cmd/main.go From 734deccad465c8d5438efda85492aece98fdfbf0 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 24 Sep 2022 21:02:09 +0100 Subject: [PATCH 073/246] default port 8888 --- cmd/main.go | 2 +- service/service.go | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/main.go b/cmd/main.go index 42125e73..e2ebf257 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -33,7 +33,7 @@ func main() { func run() error { // ? want to move to viper ASAP - port := getEnv("PORT", "8889") + port := getEnv("PORT", "8888") sCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) defer cancel() diff --git a/service/service.go b/service/service.go index 58007624..beb3d157 100644 --- a/service/service.go +++ b/service/service.go @@ -51,6 +51,12 @@ func New(r chi.Router) *Service { return s } +func DefaultService() *Service { + s := &Service{chi.NewMux(), log.Default(), ws.DefaultClient} + s.routes() + return s +} + func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { h.Respond(w, r, data, status) } From adaedf509197322e07620a1cb98fe0847611bcc8 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Sun, 25 Sep 2022 01:42:15 +0330 Subject: [PATCH 074/246] new CLI for server --- cmd/env_test.go | 24 +-- cmd/main.go | 178 ++++++++++++------ go.mod | 21 +-- go.sum | 475 ++---------------------------------------------- 4 files changed, 153 insertions(+), 545 deletions(-) diff --git a/cmd/env_test.go b/cmd/env_test.go index bce52321..204ea968 100644 --- a/cmd/env_test.go +++ b/cmd/env_test.go @@ -1,16 +1,16 @@ package main -import ( - "testing" -) +// import ( +// "testing" +// ) -func TestEnv(t *testing.T) { - // backend server address/port flag - // frontend address/port flag - // timeout - read, write, idle - // +// func TestEnv(t *testing.T) { +// // backend server address/port flag +// // frontend address/port flag +// // timeout - read, write, idle +// // - if err := loadConfig(); err != nil { - t.Fatal(err) - } -} +// if err := loadConfig(); err != nil { +// t.Fatal(err) +// } +// } diff --git a/cmd/main.go b/cmd/main.go index e2ebf257..1827bbb3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,39 +2,101 @@ package main import ( "context" - "flag" + "errors" + "fmt" "log" "net" "net/http" "os" "os/signal" - "path/filepath" - "runtime" + "strconv" "syscall" "time" "github.com/go-chi/chi/v5" "github.com/rs/cors" - "github.com/spf13/viper" + "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" "golang.org/x/sync/errgroup" "github.com/rog-golang-buddies/rapidmidiex/service" ) +// var ( +// port = flag.Int("SERVER_PORT", 8888, "The port that the server will be running on") +// ) + +type config struct { + Port int `json:"port"` +} + var ( - port = flag.Int("SERVER_PORT", 8888, "The port that the server will be running on") + errInvalidPort = errors.New("invalid port number") ) func main() { - if err := run(); err != nil { + flags := []cli.Flag{ + altsrc.NewIntFlag(&cli.IntFlag{ + Name: "port", + Value: 0, + Usage: "Defines the port which server should listen on", + Required: false, + Aliases: []string{"p"}, + EnvVars: []string{"PORT"}, + }), + &cli.StringFlag{ + Name: "load", + Aliases: []string{"l"}, + }, + } + + c := &cli.App{ + Name: "rmx", + Usage: "RapidMidiEx Server CLI", + Action: func(*cli.Context) error { + return nil + }, + Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")), + Commands: []*cli.Command{ + { + Name: "start", + Category: "run", + Aliases: []string{"s"}, + Description: "Starts the server in production mode.", + Action: func(cCtx *cli.Context) error { + port := cCtx.Int("port") + fmt.Println(port) + if port < 0 { + return errInvalidPort + } + + cfg := &config{ + Port: port, + } + + return run(cfg) + }, + Flags: flags, + }, + { + Name: "dev", + Category: "run", + Aliases: []string{"d"}, + Description: "Starts the server in development mode", + Action: func(cCtx *cli.Context) error { + return nil + }, + Flags: flags, + }, + }, + } + + if err := c.Run(os.Args); err != nil { log.Fatalln(err) } } -func run() error { - // ? want to move to viper ASAP - port := getEnv("PORT", "8888") - +func run(cfg *config) error { sCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) defer cancel() @@ -47,7 +109,7 @@ func run() error { } srv := http.Server{ - Addr: ":" + port, + Addr: ":" + strconv.Itoa(cfg.Port), Handler: cors.New(c).Handler(service.New(chi.NewMux())), // max time to read request from the client ReadTimeout: 10 * time.Second, @@ -79,56 +141,56 @@ func run() error { return g.Wait() } -func getEnv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - return fallback -} - -func init() { - // // name of config file (without extension) - // viper.SetConfigName("config") - // // REQUIRED if the config file does not have the extension in the name - // viper.SetConfigType("env") - // // optionally look for config in the working directory - // viper.AddConfigPath(".") - - //// Set Default variables - // viper.SetDefault("PORT", "8080") +// func getEnv(key, fallback string) string { +// if value, ok := os.LookupEnv(key); ok { +// return value +// } +// return fallback +// } - // viper.AutomaticEnv() +// func init() { +// // // name of config file (without extension) +// // viper.SetConfigName("config") +// // // REQUIRED if the config file does not have the extension in the name +// // viper.SetConfigType("env") +// // // optionally look for config in the working directory +// // viper.AddConfigPath(".") - // if err := viper.ReadInConfig(); err != nil { - // panic(err) - // } -} +// //// Set Default variables +// // viper.SetDefault("PORT", "8080") -// func LoadConfig(path string) (config Config, err error) { -// // Read file path -// viper.AddConfigPath(path) -// // set config file and path -// viper.SetConfigName("app") -// viper.SetConfigType("env") -// // watching changes in app.env -// viper.AutomaticEnv() -// // reading the config file -// err = viper.ReadInConfig() -// if err != nil { -// return -// } +// // viper.AutomaticEnv() -// err = viper.Unmarshal(&config) -// return +// // if err := viper.ReadInConfig(); err != nil { +// // panic(err) +// // } // } -func loadConfig() error { - _, b, _, _ := runtime.Caller(0) - basepath := filepath.Join(filepath.Dir(b), "../") - viper.SetConfigFile(basepath + ".env") - // viper.AddConfigPath("../") - viper.SetConfigType("dotenv") - // viper.SetConfigFile(".env") - - return viper.ReadInConfig() -} +// // func LoadConfig(path string) (config Config, err error) { +// // // Read file path +// // viper.AddConfigPath(path) +// // // set config file and path +// // viper.SetConfigName("app") +// // viper.SetConfigType("env") +// // // watching changes in app.env +// // viper.AutomaticEnv() +// // // reading the config file +// // err = viper.ReadInConfig() +// // if err != nil { +// // return +// // } + +// // err = viper.Unmarshal(&config) +// // return +// // } + +// func loadConfig() error { +// _, b, _, _ := runtime.Caller(0) +// basepath := filepath.Join(filepath.Dir(b), "../") +// viper.SetConfigFile(basepath + ".env") +// // viper.AddConfigPath("../") +// viper.SetConfigType("dotenv") +// // viper.SetConfigFile(".env") + +// return viper.ReadInConfig() +// } diff --git a/go.mod b/go.mod index 7f41be73..db5f138d 100644 --- a/go.mod +++ b/go.mod @@ -12,35 +12,28 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/hyphengolang/prelude v0.0.7 github.com/rs/cors v1.8.2 - github.com/spf13/viper v1.13.0 + github.com/urfave/cli/v2 v2.16.3 golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 ) require ( + github.com/BurntSushi/toml v1.1.0 // indirect github.com/containerd/console v1.0.3 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/kr/pretty v0.3.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.12.0 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/spf13/afero v1.8.2 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.4.1 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5f865796..03c658aa 100644 --- a/go.sum +++ b/go.sum @@ -1,45 +1,6 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/charmbracelet/bubbles v0.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og= github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4= @@ -49,117 +10,33 @@ github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJ github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY= github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/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.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hyphengolang/prelude v0.0.7 h1:AgLkbASkg/3ioIjr0JinhReTiaeppIaX2sa7vrFwaoc= github.com/hyphengolang/prelude v0.0.7/go.mod h1:obKnO4SQhPfu3gqA9g29nbDRQLNTtmJy02i7KYZGdfM= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -169,8 +46,6 @@ github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= @@ -183,361 +58,39 @@ github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= -github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= -github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk= +github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= golang.org/x/exp v0.0.0-20220914170420-dc92f8653013 h1:ZjglnWxEUdPyXl4o/j4T89SRCI+4X6NW6185PNLEOF4= golang.org/x/exp v0.0.0-20220914170420-dc92f8653013/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From 77d20c0f097bcb9776603e3c2ce34102a977221d Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Sat, 24 Sep 2022 23:19:10 -0400 Subject: [PATCH 075/246] Fix ws endpoint, launch config --- .vscode/launch.json | 5 +++-- ui/terminal/tui/lobbyui/lobby.go | 4 ++-- ui/terminal/tui/tui.go | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index cff0396b..9c19b925 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,13 +5,14 @@ "version": "0.2.0", "configurations": [ { - "name": "Start Server", + "name": "Start App Server (dev)", "type": "go", "request": "launch", "mode": "auto", "program": "${workspaceFolder}/cmd", "cwd": "${workspaceFolder}", - "envFile": "${workspaceFolder}/cmd/.env" + "envFile": "${workspaceFolder}/cmd/.env", + "args": ["start", "dev"] }, { "name": "TUI Client", diff --git a/ui/terminal/tui/lobbyui/lobby.go b/ui/terminal/tui/lobbyui/lobby.go index 771e75cb..c1a09353 100644 --- a/ui/terminal/tui/lobbyui/lobby.go +++ b/ui/terminal/tui/lobbyui/lobby.go @@ -257,9 +257,9 @@ type JamConnected struct { } // Commands -func jamConnect(baseURL, jamID string) tea.Cmd { +func jamConnect(wsEndpoint, jamID string) tea.Cmd { return func() tea.Msg { - url := baseURL + "/" + jamID + url := wsEndpoint + "/jam/" + jamID fmt.Println("ws url", url) ws, _, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { diff --git a/ui/terminal/tui/tui.go b/ui/terminal/tui/tui.go index cc93a8a0..eb10e12d 100644 --- a/ui/terminal/tui/tui.go +++ b/ui/terminal/tui/tui.go @@ -40,15 +40,15 @@ type mainModel struct { } func NewModel(serverHostURL string) (mainModel, error) { - wsURL, err := url.Parse(serverHostURL) + wsHostURL, err := url.Parse(serverHostURL) if err != nil { return mainModel{}, err } - wsURL.Scheme = "ws" + wsHostURL.Scheme = "ws" return mainModel{ curView: lobbyView, - lobby: lobbyui.New(wsURL.String()+"/ws", serverHostURL+"/api/v1"), + lobby: lobbyui.New(wsHostURL.String()+"/ws", serverHostURL+"/api/v1"), jam: jamui.New(), RESTendpoint: serverHostURL + "/api/v1", }, nil From 4e1a639e3fe0ec09afa6fd725674b71056e8b3b7 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Sun, 25 Sep 2022 13:16:42 +0330 Subject: [PATCH 076/246] moved cli to cli.go (contains bug) --- cmd/cli.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++ cmd/main.go | 70 +--------------------------------------------- 2 files changed, 81 insertions(+), 69 deletions(-) create mode 100644 cmd/cli.go diff --git a/cmd/cli.go b/cmd/cli.go new file mode 100644 index 00000000..b01fee77 --- /dev/null +++ b/cmd/cli.go @@ -0,0 +1,80 @@ +package main + +import ( + "errors" + "fmt" + "time" + + "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" +) + +type config struct { + Port int `json:"port"` +} + +var ( + errInvalidPort = errors.New("invalid port number") +) + +func initCLI() *cli.App { + flags := []cli.Flag{ + altsrc.NewIntFlag(&cli.IntFlag{ + Name: "port", + Value: 0, + Usage: "Defines the port which server should listen on", + Required: false, + Aliases: []string{"p"}, + EnvVars: []string{"PORT"}, + }), + &cli.StringFlag{ + Name: "load", + Aliases: []string{"l"}, + }, + } + + c := &cli.App{ + Name: "rmx", + Usage: "RapidMidiEx Server CLI", + Version: "v0.0.1", + Compiled: time.Now().UTC(), + Action: func(*cli.Context) error { + return nil + }, + Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")), + Commands: []*cli.Command{ + { + Name: "start", + Category: "run", + Aliases: []string{"s"}, + Description: "Starts the server in production mode.", + Action: func(cCtx *cli.Context) error { + port := cCtx.Int("port") + fmt.Println(port) + if port < 0 { + return errInvalidPort + } + + cfg := &config{ + Port: port, + } + + return run(cfg) + }, + Flags: flags, + }, + { + Name: "dev", + Category: "run", + Aliases: []string{"d"}, + Description: "Starts the server in development mode", + Action: func(cCtx *cli.Context) error { + return nil + }, + Flags: flags, + }, + }, + } + + return c +} diff --git a/cmd/main.go b/cmd/main.go index 1827bbb3..ae3c6c4e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,8 +2,6 @@ package main import ( "context" - "errors" - "fmt" "log" "net" "net/http" @@ -15,8 +13,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/rs/cors" - "github.com/urfave/cli/v2" - "github.com/urfave/cli/v2/altsrc" "golang.org/x/sync/errgroup" "github.com/rog-golang-buddies/rapidmidiex/service" @@ -26,72 +22,8 @@ import ( // port = flag.Int("SERVER_PORT", 8888, "The port that the server will be running on") // ) -type config struct { - Port int `json:"port"` -} - -var ( - errInvalidPort = errors.New("invalid port number") -) - func main() { - flags := []cli.Flag{ - altsrc.NewIntFlag(&cli.IntFlag{ - Name: "port", - Value: 0, - Usage: "Defines the port which server should listen on", - Required: false, - Aliases: []string{"p"}, - EnvVars: []string{"PORT"}, - }), - &cli.StringFlag{ - Name: "load", - Aliases: []string{"l"}, - }, - } - - c := &cli.App{ - Name: "rmx", - Usage: "RapidMidiEx Server CLI", - Action: func(*cli.Context) error { - return nil - }, - Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")), - Commands: []*cli.Command{ - { - Name: "start", - Category: "run", - Aliases: []string{"s"}, - Description: "Starts the server in production mode.", - Action: func(cCtx *cli.Context) error { - port := cCtx.Int("port") - fmt.Println(port) - if port < 0 { - return errInvalidPort - } - - cfg := &config{ - Port: port, - } - - return run(cfg) - }, - Flags: flags, - }, - { - Name: "dev", - Category: "run", - Aliases: []string{"d"}, - Description: "Starts the server in development mode", - Action: func(cCtx *cli.Context) error { - return nil - }, - Flags: flags, - }, - }, - } - - if err := c.Run(os.Args); err != nil { + if err := initCLI().Run(os.Args); err != nil { log.Fatalln(err) } } From 1ea68ee9d57334f8a10f5a2c523508e0f39ef6d9 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Sun, 25 Sep 2022 13:27:39 +0330 Subject: [PATCH 077/246] fixed makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1cfb99d6..f003933d 100644 --- a/Makefile +++ b/Makefile @@ -39,4 +39,4 @@ fmt: ## build_server: Build server binary into bin/ directory .PHONY: build build: - $(GOFLAGS) $(GO_BUILD) -a -v -ldflags="-w -s" -o bin/rmx-server cmd/main.go + $(GOFLAGS) $(GO_BUILD) -a -v -ldflags="-w -s" -o bin/rmx-server cmd/*.go From a99b2f75b0acdef1aa4245a2116200b5f68f491f Mon Sep 17 00:00:00 2001 From: adoublef Date: Sun, 25 Sep 2022 15:31:54 +0100 Subject: [PATCH 078/246] truncate to single file --- service/errors.go | 11 --- service/routes.go | 188 +++++++++++++++++++++++++++++++++++++++++++ service/service.go | 17 +++- service/views.go | 41 ---------- service/websocket.go | 158 ------------------------------------ 5 files changed, 202 insertions(+), 213 deletions(-) delete mode 100644 service/errors.go delete mode 100644 service/views.go delete mode 100644 service/websocket.go diff --git a/service/errors.go b/service/errors.go deleted file mode 100644 index fc4831ce..00000000 --- a/service/errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package service - -import ( - "errors" -) - -var ( - ErrNoCookie = errors.New("api: cookie not found") - ErrSessionNotFound = errors.New("api: session not found") - ErrSessionExists = errors.New("api: session already exists") -) diff --git a/service/routes.go b/service/routes.go index 0734f023..8e896605 100644 --- a/service/routes.go +++ b/service/routes.go @@ -3,9 +3,15 @@ package service import ( "net/http" + "context" + "encoding/json" + + "github.com/gorilla/websocket" rmx "github.com/rog-golang-buddies/rapidmidiex/internal" "github.com/rog-golang-buddies/rapidmidiex/internal/suid" ws "github.com/rog-golang-buddies/rapidmidiex/internal/websocket" + + t "github.com/hyphengolang/prelude/template" ) func (s Service) handleCreateRoom() http.HandlerFunc { @@ -69,3 +75,185 @@ func (s Service) handleListRooms() http.HandlerFunc { func (s Service) handlePing(w http.ResponseWriter, r *http.Request) { s.respond(w, r, nil, http.StatusNoContent) } + +func (s Service) connectionPool(p *ws.Pool) func(f http.HandlerFunc) http.HandlerFunc { + return func(f http.HandlerFunc) http.HandlerFunc { + if p != nil { + return func(w http.ResponseWriter, r *http.Request) { + f(w, r.WithContext(context.WithValue(r.Context(), roomKey, p))) + } + } + + return func(w http.ResponseWriter, r *http.Request) { + uid, err := s.parseUUID(w, r) + if err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + p, err := s.c.Get(uid) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + r = r.WithContext(context.WithValue(r.Context(), roomKey, p)) + f(w, r) + } + } +} + +func (s Service) upgradeHTTP(readBuf, writeBuf int) func(f http.HandlerFunc) http.HandlerFunc { + return func(f http.HandlerFunc) http.HandlerFunc { + u := &websocket.Upgrader{ + ReadBufferSize: readBuf, + WriteBufferSize: writeBuf, + CheckOrigin: func(r *http.Request) bool { return true }, + } + + return func(w http.ResponseWriter, r *http.Request) { + p := r.Context().Value(roomKey).(*ws.Pool) + if p.Size() == p.MaxConn { + s.respond(w, r, ws.ErrMaxConn, http.StatusUnauthorized) + return + } + + c, err := p.NewConn(w, r, u) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + r = r.WithContext(context.WithValue(r.Context(), upgradeKey, c)) + f(w, r) + } + } +} + +func (s Service) handleP2PComms() http.HandlerFunc { + type response[T any] struct { + Typ rmx.MessageTyp `json:"type"` + Payload T `json:"payload"` + } + + type join struct { + ID suid.SUID `json:"id"` + SessionID suid.SUID `json:"sessionId"` + } + + type leave struct { + ID suid.SUID `json:"id"` + SessionID suid.SUID `json:"sessionId"` + Error any `json:"err"` + } + + return func(w http.ResponseWriter, r *http.Request) { + c := r.Context().Value(upgradeKey).(*ws.Conn) + defer func() { + // ! send error when Leaving session pool + c.SendMessage(response[leave]{ + Typ: rmx.Leave, + Payload: leave{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, + }) + + c.Close() + }() + + if err := c.SendMessage(response[join]{ + Typ: rmx.Join, + Payload: join{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, + }); err != nil { + s.l.Println(err) + return + } + + // ? could the API be adjusted such that + // ? this for-loop only needs to read and + // ? never touch the code for writing + for { + var msg response[json.RawMessage] + if err := c.ReadJSON(&msg); err != nil { + s.l.Println(err) + return + } + + // * here the message will be passed off to a different handler + // * via a go routine* + if err := c.SendMessage(response[int]{Typ: rmx.Message, Payload: 10}); err != nil { + s.l.Println(err) + return + } + } + } +} + +func (s Service) handleEcho() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + c := r.Context().Value(upgradeKey).(*ws.Conn) + defer func() { + // ! send error when Leaving session pool + c.SendMessage("leave") + + s.l.Println("default leave") + + c.Close() + }() + + if err := c.SendMessage("join"); err != nil { + s.l.Println(err) + return + } + + s.l.Println("default join") + + for { + var msg any + if err := c.ReadJSON(&msg); err != nil { + s.l.Println(err) + return + } + + if err := c.SendMessage(msg); err != nil { + s.l.Println(err) + return + } + } + } +} + +// ! to be discarded + +func (s *Service) indexHTML(path string) http.HandlerFunc { + render, err := t.Render(path) + if err != nil { + panic(err) + } + + return func(w http.ResponseWriter, r *http.Request) { + render(w, r, nil) + } +} + +func (s *Service) jamSessionHTML(path string) http.HandlerFunc { + render, err := t.Render(path) + if err != nil { + panic(err) + } + + // ! I should be rendering a 404 page if there is an error + // ! in this layer, but for an MVC this will do + return func(w http.ResponseWriter, r *http.Request) { + uid, err := s.parseUUID(w, r) + if err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + if _, err = s.c.Get(uid); err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + render(w, r, nil) + } +} diff --git a/service/service.go b/service/service.go index beb3d157..75115904 100644 --- a/service/service.go +++ b/service/service.go @@ -1,6 +1,7 @@ package service import ( + "errors" "log" "net/http" @@ -22,6 +23,12 @@ var ( func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } +var ( + ErrNoCookie = errors.New("api: cookie not found") + ErrSessionNotFound = errors.New("api: session not found") + ErrSessionExists = errors.New("api: session already exists") +) + type Session struct { ID suid.SUID `json:"id"` Name string `json:"name,omitempty"` @@ -61,9 +68,13 @@ func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, stat h.Respond(w, r, data, status) } -// ! deprecated -// func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { return h.Decode(w, r, data) } -// func (s *Service) fileServer(prefix string, dirname string) http.Handler { return h.FileServer(prefix, dirname) } +func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { + return h.Decode(w, r, data) +} + +func (s *Service) fileServer(prefix string, dirname string) http.Handler { + return h.FileServer(prefix, dirname) +} func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { return suid.ParseString(chi.URLParam(r, "id")) diff --git a/service/views.go b/service/views.go deleted file mode 100644 index e4cbfdaf..00000000 --- a/service/views.go +++ /dev/null @@ -1,41 +0,0 @@ -package service - -import "net/http" -import t "github.com/hyphengolang/prelude/template" - -// ! to be discarded - -func (s *Service) indexHTML(path string) http.HandlerFunc { - render, err := t.Render(path) - if err != nil { - panic(err) - } - - return func(w http.ResponseWriter, r *http.Request) { - render(w, r, nil) - } -} - -func (s *Service) jamSessionHTML(path string) http.HandlerFunc { - render, err := t.Render(path) - if err != nil { - panic(err) - } - - // ! I should be rendering a 404 page if there is an error - // ! in this layer, but for an MVC this will do - return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r) - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - if _, err = s.c.Get(uid); err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - render(w, r, nil) - } -} diff --git a/service/websocket.go b/service/websocket.go deleted file mode 100644 index 676a99b5..00000000 --- a/service/websocket.go +++ /dev/null @@ -1,158 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "net/http" - - "github.com/gorilla/websocket" - - rmx "github.com/rog-golang-buddies/rapidmidiex/internal" - "github.com/rog-golang-buddies/rapidmidiex/internal/suid" - ws "github.com/rog-golang-buddies/rapidmidiex/internal/websocket" -) - -func (s Service) connectionPool(p *ws.Pool) func(f http.HandlerFunc) http.HandlerFunc { - return func(f http.HandlerFunc) http.HandlerFunc { - if p != nil { - return func(w http.ResponseWriter, r *http.Request) { - f(w, r.WithContext(context.WithValue(r.Context(), roomKey, p))) - } - } - - return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r) - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - p, err := s.c.Get(uid) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - r = r.WithContext(context.WithValue(r.Context(), roomKey, p)) - f(w, r) - } - } -} - -func (s Service) upgradeHTTP(readBuf, writeBuf int) func(f http.HandlerFunc) http.HandlerFunc { - return func(f http.HandlerFunc) http.HandlerFunc { - u := &websocket.Upgrader{ - ReadBufferSize: readBuf, - WriteBufferSize: writeBuf, - CheckOrigin: func(r *http.Request) bool { return true }, - } - - return func(w http.ResponseWriter, r *http.Request) { - p := r.Context().Value(roomKey).(*ws.Pool) - if p.Size() == p.MaxConn { - s.respond(w, r, ws.ErrMaxConn, http.StatusUnauthorized) - return - } - - c, err := p.NewConn(w, r, u) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - r = r.WithContext(context.WithValue(r.Context(), upgradeKey, c)) - f(w, r) - } - } -} - -func (s Service) handleP2PComms() http.HandlerFunc { - type response[T any] struct { - Typ rmx.MessageTyp `json:"type"` - Payload T `json:"payload"` - } - - type join struct { - ID suid.SUID `json:"id"` - SessionID suid.SUID `json:"sessionId"` - } - - type leave struct { - ID suid.SUID `json:"id"` - SessionID suid.SUID `json:"sessionId"` - Error any `json:"err"` - } - - return func(w http.ResponseWriter, r *http.Request) { - c := r.Context().Value(upgradeKey).(*ws.Conn) - defer func() { - // ! send error when Leaving session pool - c.SendMessage(response[leave]{ - Typ: rmx.Leave, - Payload: leave{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, - }) - - c.Close() - }() - - if err := c.SendMessage(response[join]{ - Typ: rmx.Join, - Payload: join{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, - }); err != nil { - s.l.Println(err) - return - } - - // ? could the API be adjusted such that - // ? this for-loop only needs to read and - // ? never touch the code for writing - for { - var msg response[json.RawMessage] - if err := c.ReadJSON(&msg); err != nil { - s.l.Println(err) - return - } - - // * here the message will be passed off to a different handler - // * via a go routine* - if err := c.SendMessage(response[int]{Typ: rmx.Message, Payload: 10}); err != nil { - s.l.Println(err) - return - } - } - } -} - -func (s Service) handleEcho() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - c := r.Context().Value(upgradeKey).(*ws.Conn) - defer func() { - // ! send error when Leaving session pool - c.SendMessage("leave") - - s.l.Println("default leave") - - c.Close() - }() - - if err := c.SendMessage("join"); err != nil { - s.l.Println(err) - return - } - - s.l.Println("default join") - - for { - var msg any - if err := c.ReadJSON(&msg); err != nil { - s.l.Println(err) - return - } - - if err := c.SendMessage(msg); err != nil { - s.l.Println(err) - return - } - } - } -} From 3d0333750ed869a195fdfc54e0f8a8e846287d11 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sun, 25 Sep 2022 15:33:08 +0100 Subject: [PATCH 079/246] errInvalidPort -> ErrInvalidPort --- cmd/cli.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/cli.go b/cmd/cli.go index b01fee77..e783a90e 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -14,7 +14,7 @@ type config struct { } var ( - errInvalidPort = errors.New("invalid port number") + ErrInvalidPort = errors.New("invalid port number") ) func initCLI() *cli.App { @@ -52,7 +52,7 @@ func initCLI() *cli.App { port := cCtx.Int("port") fmt.Println(port) if port < 0 { - return errInvalidPort + return ErrInvalidPort } cfg := &config{ From 47a4eb57550a0c11973ea3f46999f1f2894b2fed Mon Sep 17 00:00:00 2001 From: adoublef Date: Sun, 25 Sep 2022 15:34:07 +0100 Subject: [PATCH 080/246] custom type has exported fields; made it exported --- cmd/cli.go | 4 ++-- cmd/main.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/cli.go b/cmd/cli.go index e783a90e..1a8ebe53 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -9,7 +9,7 @@ import ( "github.com/urfave/cli/v2/altsrc" ) -type config struct { +type Config struct { Port int `json:"port"` } @@ -55,7 +55,7 @@ func initCLI() *cli.App { return ErrInvalidPort } - cfg := &config{ + cfg := &Config{ Port: port, } diff --git a/cmd/main.go b/cmd/main.go index ae3c6c4e..0e9ab9ba 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -28,7 +28,7 @@ func main() { } } -func run(cfg *config) error { +func run(cfg *Config) error { sCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) defer cancel() From 0baf08515933c8294af857f3b4be7f6a2568ef3f Mon Sep 17 00:00:00 2001 From: pmoieni Date: Mon, 26 Sep 2022 00:47:49 +0330 Subject: [PATCH 081/246] rename from rapidmidiex to rmx --- cmd/main.go | 2 +- go.mod | 21 ++++++++------------- go.sum | 28 ++++++++-------------------- internal/websocket/client.go | 4 ++-- internal/websocket/conn.go | 4 ++-- internal/websocket/pool.go | 2 +- service/routes.go | 6 +++--- service/routes_test.go | 2 +- service/service.go | 4 ++-- ui/terminal/main.go | 2 +- ui/terminal/tui/tui.go | 6 +++--- 11 files changed, 32 insertions(+), 49 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 0e9ab9ba..50a219e0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -15,7 +15,7 @@ import ( "github.com/rs/cors" "golang.org/x/sync/errgroup" - "github.com/rog-golang-buddies/rapidmidiex/service" + "github.com/rog-golang-buddies/rmx/service" ) // var ( diff --git a/go.mod b/go.mod index db5f138d..a67ffdf5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module github.com/rog-golang-buddies/rapidmidiex +module github.com/rog-golang-buddies/rmx -go 1.18 +go 1.19 require ( github.com/charmbracelet/bubbles v0.14.0 @@ -11,17 +11,18 @@ require ( github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 github.com/hyphengolang/prelude v0.0.7 + github.com/lithammer/shortuuid/v4 v4.0.0 github.com/rs/cors v1.8.2 github.com/urfave/cli/v2 v2.16.3 - golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 + golang.org/x/exp v0.0.0-20220921164117-439092de6870 + golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 + golang.org/x/term v0.0.0-20220919170432-7a66f970e087 ) require ( github.com/BurntSushi/toml v1.1.0 // indirect github.com/containerd/console v1.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/kr/pretty v0.3.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -29,17 +30,11 @@ require ( github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.12.0 // indirect + github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) - -require ( - github.com/lithammer/shortuuid/v4 v4.0.0 - golang.org/x/exp v0.0.0-20220914170420-dc92f8653013 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect golang.org/x/text v0.3.7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 03c658aa..80e92bf1 100644 --- a/go.sum +++ b/go.sum @@ -14,7 +14,6 @@ github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARu github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= @@ -25,13 +24,6 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hyphengolang/prelude v0.0.7 h1:AgLkbASkg/3ioIjr0JinhReTiaeppIaX2sa7vrFwaoc= github.com/hyphengolang/prelude v0.0.7/go.mod h1:obKnO4SQhPfu3gqA9g29nbDRQLNTtmJy02i7KYZGdfM= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= @@ -55,14 +47,11 @@ github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIW github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= -github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -72,10 +61,10 @@ github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk= github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -golang.org/x/exp v0.0.0-20220914170420-dc92f8653013 h1:ZjglnWxEUdPyXl4o/j4T89SRCI+4X6NW6185PNLEOF4= -golang.org/x/exp v0.0.0-20220914170420-dc92f8653013/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc= -golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/exp v0.0.0-20220921164117-439092de6870 h1:j8b6j9gzSigH28O5SjSpQSSh9lFd6f5D/q0aHjNTulc= +golang.org/x/exp v0.0.0-20220921164117-439092de6870/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc= +golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -83,14 +72,13 @@ golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w= +golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/websocket/client.go b/internal/websocket/client.go index 51244e51..fa3106a1 100644 --- a/internal/websocket/client.go +++ b/internal/websocket/client.go @@ -3,8 +3,8 @@ package websocket import ( "sync" - rmx "github.com/rog-golang-buddies/rapidmidiex/internal" - "github.com/rog-golang-buddies/rapidmidiex/internal/suid" + rmx "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/suid" "golang.org/x/exp/maps" ) diff --git a/internal/websocket/conn.go b/internal/websocket/conn.go index 96932c70..5aaf016a 100644 --- a/internal/websocket/conn.go +++ b/internal/websocket/conn.go @@ -2,8 +2,8 @@ package websocket import ( "github.com/gorilla/websocket" - rmx "github.com/rog-golang-buddies/rapidmidiex/internal" - "github.com/rog-golang-buddies/rapidmidiex/internal/suid" + rmx "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/suid" ) type Conn struct { diff --git a/internal/websocket/pool.go b/internal/websocket/pool.go index 8e92d054..97192cf2 100644 --- a/internal/websocket/pool.go +++ b/internal/websocket/pool.go @@ -5,7 +5,7 @@ import ( "sync" "github.com/gorilla/websocket" - "github.com/rog-golang-buddies/rapidmidiex/internal/suid" + "github.com/rog-golang-buddies/rmx/internal/suid" // https://stackoverflow.com/questions/21362950/getting-a-slice-of-keys-from-a-map diff --git a/service/routes.go b/service/routes.go index 8e896605..80b584e3 100644 --- a/service/routes.go +++ b/service/routes.go @@ -7,9 +7,9 @@ import ( "encoding/json" "github.com/gorilla/websocket" - rmx "github.com/rog-golang-buddies/rapidmidiex/internal" - "github.com/rog-golang-buddies/rapidmidiex/internal/suid" - ws "github.com/rog-golang-buddies/rapidmidiex/internal/websocket" + rmx "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/suid" + ws "github.com/rog-golang-buddies/rmx/internal/websocket" t "github.com/hyphengolang/prelude/template" ) diff --git a/service/routes_test.go b/service/routes_test.go index 7116ebd5..995e3367 100644 --- a/service/routes_test.go +++ b/service/routes_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/go-chi/chi/v5" - // "github.com/rog-golang-buddies/rapidmidiex/internal/websocket" + // "github.com/rog-golang-buddies/rmx/internal/websocket" ) func TestRoutes(t *testing.T) { diff --git a/service/service.go b/service/service.go index 75115904..84c61e50 100644 --- a/service/service.go +++ b/service/service.go @@ -10,8 +10,8 @@ import ( h "github.com/hyphengolang/prelude/http" - "github.com/rog-golang-buddies/rapidmidiex/internal/suid" - ws "github.com/rog-golang-buddies/rapidmidiex/internal/websocket" + "github.com/rog-golang-buddies/rmx/internal/suid" + ws "github.com/rog-golang-buddies/rmx/internal/websocket" ) type contextKey string diff --git a/ui/terminal/main.go b/ui/terminal/main.go index 8def5a0d..63a84a92 100644 --- a/ui/terminal/main.go +++ b/ui/terminal/main.go @@ -1,6 +1,6 @@ package main -import "github.com/rog-golang-buddies/rapidmidiex/ui/terminal/tui" +import "github.com/rog-golang-buddies/rmx/ui/terminal/tui" func main() { tui.Run() diff --git a/ui/terminal/tui/tui.go b/ui/terminal/tui/tui.go index eb10e12d..94b79896 100644 --- a/ui/terminal/tui/tui.go +++ b/ui/terminal/tui/tui.go @@ -7,9 +7,9 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/gorilla/websocket" - "github.com/rog-golang-buddies/rapidmidiex/internal/suid" - "github.com/rog-golang-buddies/rapidmidiex/ui/terminal/tui/jamui" - "github.com/rog-golang-buddies/rapidmidiex/ui/terminal/tui/lobbyui" + "github.com/rog-golang-buddies/rmx/internal/suid" + "github.com/rog-golang-buddies/rmx/ui/terminal/tui/jamui" + "github.com/rog-golang-buddies/rmx/ui/terminal/tui/lobbyui" ) // ******** From 7b246d5fcf0e9caf287169b7b350167e3834f3d9 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Tue, 27 Sep 2022 15:20:44 +0330 Subject: [PATCH 082/246] lspsaga config --- internal/db/schema/user_query.sql | 36 ++++++++++++++++++++++++++++++ internal/db/schema/user_schema.sql | 10 +++++++++ 2 files changed, 46 insertions(+) create mode 100644 internal/db/schema/user_query.sql create mode 100644 internal/db/schema/user_schema.sql diff --git a/internal/db/schema/user_query.sql b/internal/db/schema/user_query.sql new file mode 100644 index 00000000..8301d4af --- /dev/null +++ b/internal/db/schema/user_query.sql @@ -0,0 +1,36 @@ +-- name: GetUserByID :one +SELECT * +FROM users +WHERE id = ? +LIMIT 1; + +-- name: GetUserByEmail :one +SELECT * +FROM users +WHERE email = ? +LIMIT 1; + +-- name: ListUsers :many +SELECT * +FROM users +ORDER BY id; + +-- name: CreateUser :execresult +INSERT INTO users ( + username, + email, + password, + created_at, + updated_at, + deleted_at + ) +VALUES (?, ?, ?, ?, ?, ?); + +-- name: UpdateUser :execresult +UPDATE users + SET username = ? + WHERE id = ?; + +-- name: DeleteUser :exec +DELETE FROM users +WHERE id = ?; diff --git a/internal/db/schema/user_schema.sql b/internal/db/schema/user_schema.sql new file mode 100644 index 00000000..1d115ea2 --- /dev/null +++ b/internal/db/schema/user_schema.sql @@ -0,0 +1,10 @@ +CREATE TABLE users ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + username text NOT NULL, + email text NOT NULL, + password text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL DEFAULT NULL, + UNIQUE (email) +); From 9c3a56a37c31b45520bc2b25b1c3610cff73d4f5 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 30 Sep 2022 01:56:14 +0330 Subject: [PATCH 083/246] added User db config and other small changes --- internal/db/schema/migration.sql | 11 +++ internal/db/user/db.go | 138 ++++++++++++++++++++++++++ internal/db/user/models.go | 20 ++++ internal/db/user/user_query.sql.go | 150 +++++++++++++++++++++++++++++ service/routes.go | 26 ++--- service/service.go | 8 +- sqlc.yaml | 15 +++ 7 files changed, 354 insertions(+), 14 deletions(-) create mode 100644 internal/db/schema/migration.sql create mode 100644 internal/db/user/db.go create mode 100644 internal/db/user/models.go create mode 100644 internal/db/user/user_query.sql.go create mode 100644 sqlc.yaml diff --git a/internal/db/schema/migration.sql b/internal/db/schema/migration.sql new file mode 100644 index 00000000..b610edfe --- /dev/null +++ b/internal/db/schema/migration.sql @@ -0,0 +1,11 @@ +-- +migrate UP +CREATE TABLE users ( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + username text NOT NULL, + email text NOT NULL, + password text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL DEFAULT NULL, + UNIQUE (email) +); diff --git a/internal/db/user/db.go b/internal/db/user/db.go new file mode 100644 index 00000000..ed263c95 --- /dev/null +++ b/internal/db/user/db.go @@ -0,0 +1,138 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.15.0 + +package user + +import ( + "context" + "database/sql" + "fmt" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +func Prepare(ctx context.Context, db DBTX) (*Queries, error) { + q := Queries{db: db} + var err error + if q.createUserStmt, err = db.PrepareContext(ctx, createUser); err != nil { + return nil, fmt.Errorf("error preparing query CreateUser: %w", err) + } + if q.deleteUserStmt, err = db.PrepareContext(ctx, deleteUser); err != nil { + return nil, fmt.Errorf("error preparing query DeleteUser: %w", err) + } + if q.getUserByEmailStmt, err = db.PrepareContext(ctx, getUserByEmail); err != nil { + return nil, fmt.Errorf("error preparing query GetUserByEmail: %w", err) + } + if q.getUserByIDStmt, err = db.PrepareContext(ctx, getUserByID); err != nil { + return nil, fmt.Errorf("error preparing query GetUserByID: %w", err) + } + if q.listUsersStmt, err = db.PrepareContext(ctx, listUsers); err != nil { + return nil, fmt.Errorf("error preparing query ListUsers: %w", err) + } + if q.updateUserStmt, err = db.PrepareContext(ctx, updateUser); err != nil { + return nil, fmt.Errorf("error preparing query UpdateUser: %w", err) + } + return &q, nil +} + +func (q *Queries) Close() error { + var err error + if q.createUserStmt != nil { + if cerr := q.createUserStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createUserStmt: %w", cerr) + } + } + if q.deleteUserStmt != nil { + if cerr := q.deleteUserStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing deleteUserStmt: %w", cerr) + } + } + if q.getUserByEmailStmt != nil { + if cerr := q.getUserByEmailStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getUserByEmailStmt: %w", cerr) + } + } + if q.getUserByIDStmt != nil { + if cerr := q.getUserByIDStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getUserByIDStmt: %w", cerr) + } + } + if q.listUsersStmt != nil { + if cerr := q.listUsersStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing listUsersStmt: %w", cerr) + } + } + if q.updateUserStmt != nil { + if cerr := q.updateUserStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing updateUserStmt: %w", cerr) + } + } + return err +} + +func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (sql.Result, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...) + case stmt != nil: + return stmt.ExecContext(ctx, args...) + default: + return q.db.ExecContext(ctx, query, args...) + } +} + +func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...) + case stmt != nil: + return stmt.QueryContext(ctx, args...) + default: + return q.db.QueryContext(ctx, query, args...) + } +} + +func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) *sql.Row { + switch { + case stmt != nil && q.tx != nil: + return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...) + case stmt != nil: + return stmt.QueryRowContext(ctx, args...) + default: + return q.db.QueryRowContext(ctx, query, args...) + } +} + +type Queries struct { + db DBTX + tx *sql.Tx + createUserStmt *sql.Stmt + deleteUserStmt *sql.Stmt + getUserByEmailStmt *sql.Stmt + getUserByIDStmt *sql.Stmt + listUsersStmt *sql.Stmt + updateUserStmt *sql.Stmt +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + tx: tx, + createUserStmt: q.createUserStmt, + deleteUserStmt: q.deleteUserStmt, + getUserByEmailStmt: q.getUserByEmailStmt, + getUserByIDStmt: q.getUserByIDStmt, + listUsersStmt: q.listUsersStmt, + updateUserStmt: q.updateUserStmt, + } +} diff --git a/internal/db/user/models.go b/internal/db/user/models.go new file mode 100644 index 00000000..e7e131ac --- /dev/null +++ b/internal/db/user/models.go @@ -0,0 +1,20 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.15.0 + +package user + +import ( + "database/sql" + "time" +) + +type User struct { + ID int64 `db:"id" json:"id"` + Username string `db:"username" json:"username"` + Email string `db:"email" json:"email"` + Password string `db:"password" json:"password"` + CreatedAt time.Time `db:"created_at" json:"createdAt"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updatedAt"` + DeletedAt sql.NullTime `db:"deleted_at" json:"deletedAt"` +} diff --git a/internal/db/user/user_query.sql.go b/internal/db/user/user_query.sql.go new file mode 100644 index 00000000..a8903f4f --- /dev/null +++ b/internal/db/user/user_query.sql.go @@ -0,0 +1,150 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.15.0 +// source: user_query.sql + +package user + +import ( + "context" + "database/sql" + "time" +) + +const createUser = `-- name: CreateUser :execresult +INSERT INTO users ( + username, + email, + password, + created_at, + updated_at, + deleted_at + ) +VALUES (?, ?, ?, ?, ?, ?) +` + +type CreateUserParams struct { + Username string `db:"username" json:"username"` + Email string `db:"email" json:"email"` + Password string `db:"password" json:"password"` + CreatedAt time.Time `db:"created_at" json:"createdAt"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updatedAt"` + DeletedAt sql.NullTime `db:"deleted_at" json:"deletedAt"` +} + +func (q *Queries) CreateUser(ctx context.Context, arg *CreateUserParams) (sql.Result, error) { + return q.exec(ctx, q.createUserStmt, createUser, + arg.Username, + arg.Email, + arg.Password, + arg.CreatedAt, + arg.UpdatedAt, + arg.DeletedAt, + ) +} + +const deleteUser = `-- name: DeleteUser :exec +DELETE FROM users +WHERE id = ? +` + +func (q *Queries) DeleteUser(ctx context.Context, id int64) error { + _, err := q.exec(ctx, q.deleteUserStmt, deleteUser, id) + return err +} + +const getUserByEmail = `-- name: GetUserByEmail :one +SELECT id, username, email, password, created_at, updated_at, deleted_at +FROM users +WHERE email = ? +LIMIT 1 +` + +func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { + row := q.queryRow(ctx, q.getUserByEmailStmt, getUserByEmail, email) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.Password, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} + +const getUserByID = `-- name: GetUserByID :one +SELECT id, username, email, password, created_at, updated_at, deleted_at +FROM users +WHERE id = ? +LIMIT 1 +` + +func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) { + row := q.queryRow(ctx, q.getUserByIDStmt, getUserByID, id) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.Password, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ) + return i, err +} + +const listUsers = `-- name: ListUsers :many +SELECT id, username, email, password, created_at, updated_at, deleted_at +FROM users +ORDER BY id +` + +func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { + rows, err := q.query(ctx, q.listUsersStmt, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + items := []User{} + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.Password, + &i.CreatedAt, + &i.UpdatedAt, + &i.DeletedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateUser = `-- name: UpdateUser :execresult +UPDATE users + SET username = ? + WHERE id = ? +` + +type UpdateUserParams struct { + Username string `db:"username" json:"username"` + ID int64 `db:"id" json:"id"` +} + +func (q *Queries) UpdateUser(ctx context.Context, arg *UpdateUserParams) (sql.Result, error) { + return q.exec(ctx, q.updateUserStmt, updateUser, arg.Username, arg.ID) +} diff --git a/service/routes.go b/service/routes.go index 80b584e3..7a1de526 100644 --- a/service/routes.go +++ b/service/routes.go @@ -14,7 +14,7 @@ import ( t "github.com/hyphengolang/prelude/template" ) -func (s Service) handleCreateRoom() http.HandlerFunc { +func (s *Service) handleCreateRoom() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, err := s.c.NewPool(4) if err != nil { @@ -22,13 +22,13 @@ func (s Service) handleCreateRoom() http.HandlerFunc { return } - v := Session{ID: suid.FromUUID(uid)} + v := &session{ID: suid.FromUUID(uid)} s.respond(w, r, v, http.StatusOK) } } -func (s Service) handleGetRoom() http.HandlerFunc { +func (s *Service) handleGetRoom() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { uid, err := s.parseUUID(w, r) if err != nil { @@ -43,7 +43,7 @@ func (s Service) handleGetRoom() http.HandlerFunc { return } - v := &Session{ + v := &session{ ID: p.ID.ShortUUID(), Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), } @@ -52,15 +52,15 @@ func (s Service) handleGetRoom() http.HandlerFunc { } } -func (s Service) handleListRooms() http.HandlerFunc { +func (s *Service) handleListRooms() http.HandlerFunc { type response struct { - Sessions []Session `json:"sessions"` + Sessions []session `json:"sessions"` } return func(w http.ResponseWriter, r *http.Request) { v := &response{ - Sessions: rmx.FMap(s.c.List(), func(p *ws.Pool) Session { - return Session{ + Sessions: rmx.FMap(s.c.List(), func(p *ws.Pool) session { + return session{ ID: p.ID.ShortUUID(), Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), UserCount: p.Size(), @@ -72,11 +72,11 @@ func (s Service) handleListRooms() http.HandlerFunc { } } -func (s Service) handlePing(w http.ResponseWriter, r *http.Request) { +func (s *Service) handlePing(w http.ResponseWriter, r *http.Request) { s.respond(w, r, nil, http.StatusNoContent) } -func (s Service) connectionPool(p *ws.Pool) func(f http.HandlerFunc) http.HandlerFunc { +func (s *Service) connectionPool(p *ws.Pool) func(f http.HandlerFunc) http.HandlerFunc { return func(f http.HandlerFunc) http.HandlerFunc { if p != nil { return func(w http.ResponseWriter, r *http.Request) { @@ -103,7 +103,7 @@ func (s Service) connectionPool(p *ws.Pool) func(f http.HandlerFunc) http.Handle } } -func (s Service) upgradeHTTP(readBuf, writeBuf int) func(f http.HandlerFunc) http.HandlerFunc { +func (s *Service) upgradeHTTP(readBuf, writeBuf int) func(f http.HandlerFunc) http.HandlerFunc { return func(f http.HandlerFunc) http.HandlerFunc { u := &websocket.Upgrader{ ReadBufferSize: readBuf, @@ -130,7 +130,7 @@ func (s Service) upgradeHTTP(readBuf, writeBuf int) func(f http.HandlerFunc) htt } } -func (s Service) handleP2PComms() http.HandlerFunc { +func (s *Service) handleP2PComms() http.HandlerFunc { type response[T any] struct { Typ rmx.MessageTyp `json:"type"` Payload T `json:"payload"` @@ -187,7 +187,7 @@ func (s Service) handleP2PComms() http.HandlerFunc { } } -func (s Service) handleEcho() http.HandlerFunc { +func (s *Service) handleEcho() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { c := r.Context().Value(upgradeKey).(*ws.Conn) defer func() { diff --git a/service/service.go b/service/service.go index 84c61e50..e7f04d53 100644 --- a/service/service.go +++ b/service/service.go @@ -29,7 +29,13 @@ var ( ErrSessionExists = errors.New("api: session already exists") ) -type Session struct { +type jam struct { + Name string `json:"name"` + BPM int `json:"bpm"` + ws.Pool +} + +type session struct { ID suid.SUID `json:"id"` Name string `json:"name,omitempty"` Users []suid.SUID `json:"users,omitempty"` diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 00000000..351737fc --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,15 @@ +version: '2' +sql: + - schema: 'internal/db/schema/user_schema.sql' + queries: 'internal/db/schema/user_query.sql' + engine: 'mysql' + gen: + go: + package: 'user' + out: 'internal/db/user' + emit_db_tags: true + emit_prepared_queries: true + emit_empty_slices: true + emit_params_struct_pointers: true + emit_json_tags: true + json_tags_case_style: 'camel' From 2f8fc458015167f5339cf20250465c8d9858043e Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 30 Sep 2022 19:16:28 +0100 Subject: [PATCH 084/246] db/ root-package && splitting up logic in service/ --- {internal/db => db}/schema/migration.sql | 0 {internal/db => db}/schema/user_query.sql | 0 {internal/db => db}/schema/user_schema.sql | 0 {internal/db => db}/user/db.go | 0 {internal/db => db}/user/models.go | 0 {internal/db => db}/user/user_query.sql.go | 0 internal/errors.go | 7 -- internal/{utils.go => fp/fp.go} | 2 +- internal/rmx.go | 9 +- service/jam/jam.go | 99 ++++++++++++++++++++++ service/{ => jam}/routes.go | 9 +- service/jam/routes_test.go | 27 ++++++ service/service.go | 81 +----------------- service/user/user.go | 65 ++++++++++++++ 14 files changed, 204 insertions(+), 95 deletions(-) rename {internal/db => db}/schema/migration.sql (100%) rename {internal/db => db}/schema/user_query.sql (100%) rename {internal/db => db}/schema/user_schema.sql (100%) rename {internal/db => db}/user/db.go (100%) rename {internal/db => db}/user/models.go (100%) rename {internal/db => db}/user/user_query.sql.go (100%) delete mode 100644 internal/errors.go rename internal/{utils.go => fp/fp.go} (88%) create mode 100644 service/jam/jam.go rename service/{ => jam}/routes.go (94%) create mode 100644 service/jam/routes_test.go create mode 100644 service/user/user.go diff --git a/internal/db/schema/migration.sql b/db/schema/migration.sql similarity index 100% rename from internal/db/schema/migration.sql rename to db/schema/migration.sql diff --git a/internal/db/schema/user_query.sql b/db/schema/user_query.sql similarity index 100% rename from internal/db/schema/user_query.sql rename to db/schema/user_query.sql diff --git a/internal/db/schema/user_schema.sql b/db/schema/user_schema.sql similarity index 100% rename from internal/db/schema/user_schema.sql rename to db/schema/user_schema.sql diff --git a/internal/db/user/db.go b/db/user/db.go similarity index 100% rename from internal/db/user/db.go rename to db/user/db.go diff --git a/internal/db/user/models.go b/db/user/models.go similarity index 100% rename from internal/db/user/models.go rename to db/user/models.go diff --git a/internal/db/user/user_query.sql.go b/db/user/user_query.sql.go similarity index 100% rename from internal/db/user/user_query.sql.go rename to db/user/user_query.sql.go diff --git a/internal/errors.go b/internal/errors.go deleted file mode 100644 index 1267c216..00000000 --- a/internal/errors.go +++ /dev/null @@ -1,7 +0,0 @@ -package internal - -import ( - "errors" -) - -var ErrTodo = errors.New("rmx: not yet implemented") diff --git a/internal/utils.go b/internal/fp/fp.go similarity index 88% rename from internal/utils.go rename to internal/fp/fp.go index 101b187f..6cdbeb77 100644 --- a/internal/utils.go +++ b/internal/fp/fp.go @@ -1,4 +1,4 @@ -package internal +package fp func FMap[T any, U any](vs []T, f func(T) U) (us []U) { us = make([]U, len(vs)) diff --git a/internal/rmx.go b/internal/rmx.go index 3913d3e1..3f749836 100644 --- a/internal/rmx.go +++ b/internal/rmx.go @@ -1,5 +1,11 @@ package internal +import ( + "errors" +) + +var ErrTodo = errors.New("rmx: not yet implemented") + type MessageTyp int const ( @@ -64,6 +70,3 @@ func (t *MessageTyp) UnmarshalJSON(b []byte) error { func (t MessageTyp) MarshalJSON() ([]byte, error) { return []byte(`"` + t.String() + `"`), nil } - -// ! ID with ability to encode to a URL friendly version -type ID string diff --git a/service/jam/jam.go b/service/jam/jam.go new file mode 100644 index 00000000..01d84b16 --- /dev/null +++ b/service/jam/jam.go @@ -0,0 +1,99 @@ +package jam + +import ( + "errors" + "log" + "net/http" + + "github.com/go-chi/chi/middleware" + "github.com/go-chi/chi/v5" + + h "github.com/hyphengolang/prelude/http" + + "github.com/rog-golang-buddies/rmx/internal/suid" + ws "github.com/rog-golang-buddies/rmx/internal/websocket" +) + +type contextKey string + +var ( + roomKey = contextKey("rmx-fetch-pool") + upgradeKey = contextKey("rmx-upgrade-http") +) + +func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } + +var ( + ErrNoCookie = errors.New("api: cookie not found") + ErrSessionNotFound = errors.New("api: session not found") + ErrSessionExists = errors.New("api: session already exists") +) + +type jam struct { + Name string `json:"name"` + BPM int `json:"bpm"` + ws.Pool +} + +type session struct { + ID suid.SUID `json:"id"` + Name string `json:"name,omitempty"` + Users []suid.SUID `json:"users,omitempty"` + /* Not really required */ + UserCount int `json:"userCount"` +} + +type User struct { + ID suid.SUID `json:"id"` + Name string `json:"name,omitempty"` + /* More fields can belong here */ +} + +type Service struct { + r chi.Router + l *log.Logger + + c *ws.Client +} + +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } + +func New(r chi.Router) *Service { + s := &Service{r, log.Default(), ws.DefaultClient} + s.routes() + return s +} + +func DefaultService() *Service { + s := &Service{chi.NewMux(), log.Default(), ws.DefaultClient} + s.routes() + return s +} + +func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { + h.Respond(w, r, data, status) +} + +func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { + return h.Decode(w, r, data) +} + +func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { + return suid.ParseString(chi.URLParam(r, "id")) +} + +func (s *Service) routes() { + s.r.Use(middleware.Logger) + + s.r.Route("/api/v1", func(r chi.Router) { + r.Get("/jam", s.handleListRooms()) + r.Post("/jam", s.handleCreateRoom()) + r.Get("/jam/{id}", s.handleGetRoom()) + + // health + r.Get("/ping", s.handlePing) + }) + + // Websocket + s.r.Get("/ws/jam/{id}", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.connectionPool(nil))) +} diff --git a/service/routes.go b/service/jam/routes.go similarity index 94% rename from service/routes.go rename to service/jam/routes.go index 7a1de526..63481718 100644 --- a/service/routes.go +++ b/service/jam/routes.go @@ -1,4 +1,4 @@ -package service +package jam import ( "net/http" @@ -8,6 +8,7 @@ import ( "github.com/gorilla/websocket" rmx "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/suid" ws "github.com/rog-golang-buddies/rmx/internal/websocket" @@ -45,7 +46,7 @@ func (s *Service) handleGetRoom() http.HandlerFunc { v := &session{ ID: p.ID.ShortUUID(), - Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), + Users: fp.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), } s.respond(w, r, v, http.StatusOK) @@ -59,10 +60,10 @@ func (s *Service) handleListRooms() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { v := &response{ - Sessions: rmx.FMap(s.c.List(), func(p *ws.Pool) session { + Sessions: fp.FMap(s.c.List(), func(p *ws.Pool) session { return session{ ID: p.ID.ShortUUID(), - Users: rmx.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), + Users: fp.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), UserCount: p.Size(), } }), diff --git a/service/jam/routes_test.go b/service/jam/routes_test.go new file mode 100644 index 00000000..eded74cd --- /dev/null +++ b/service/jam/routes_test.go @@ -0,0 +1,27 @@ +package jam + +import ( + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" + // "github.com/rog-golang-buddies/rmx/internal/websocket" +) + +func TestRoutes(t *testing.T) { + srv := New(chi.NewMux()) + + // srv.r.Get("/ws/echo", chain(srv.handleEcho(),srv.upgradeHTTP(1024,1024),srv.connectionPool(websocket.DefaultPool()))) + + s := httptest.NewServer(srv) + t.Cleanup(func() { s.Close() }) + + r, err := s.Client().Get(s.URL + "/api/v1/ping/") + if err != nil { + t.Fatal(err) + } + + if r.StatusCode != 204 { + t.Fatalf("expected %d;got %d", 204, r.StatusCode) + } +} diff --git a/service/service.go b/service/service.go index e7f04d53..b4bd9e74 100644 --- a/service/service.go +++ b/service/service.go @@ -1,103 +1,24 @@ package service import ( - "errors" "log" "net/http" - "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" h "github.com/hyphengolang/prelude/http" - - "github.com/rog-golang-buddies/rmx/internal/suid" - ws "github.com/rog-golang-buddies/rmx/internal/websocket" -) - -type contextKey string - -var ( - roomKey = contextKey("rmx-fetch-pool") - upgradeKey = contextKey("rmx-upgrade-http") ) func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } -var ( - ErrNoCookie = errors.New("api: cookie not found") - ErrSessionNotFound = errors.New("api: session not found") - ErrSessionExists = errors.New("api: session already exists") -) - -type jam struct { - Name string `json:"name"` - BPM int `json:"bpm"` - ws.Pool -} - -type session struct { - ID suid.SUID `json:"id"` - Name string `json:"name,omitempty"` - Users []suid.SUID `json:"users,omitempty"` - /* Not really required */ - UserCount int `json:"userCount"` -} - -type User struct { - ID suid.SUID `json:"id"` - Name string `json:"name,omitempty"` - /* More fields can belong here */ -} - type Service struct { r chi.Router l *log.Logger - - c *ws.Client } func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } func New(r chi.Router) *Service { - s := &Service{r, log.Default(), ws.DefaultClient} - s.routes() - return s -} - -func DefaultService() *Service { - s := &Service{chi.NewMux(), log.Default(), ws.DefaultClient} - s.routes() + s := &Service{r, log.Default()} return s } - -func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { - h.Respond(w, r, data, status) -} - -func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { - return h.Decode(w, r, data) -} - -func (s *Service) fileServer(prefix string, dirname string) http.Handler { - return h.FileServer(prefix, dirname) -} - -func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { - return suid.ParseString(chi.URLParam(r, "id")) -} - -func (s *Service) routes() { - s.r.Use(middleware.Logger) - - // REST v1 - s.r.Get("/api/v1/jam", s.handleListRooms()) - s.r.Post("/api/v1/jam", s.handleCreateRoom()) - s.r.Get("/api/v1/jam/{id}", s.handleGetRoom()) - - s.r.Get("/ping", s.handlePing) - - // Websocket - s.r.Get("/ws/jam/{id}", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.connectionPool(nil))) - - s.r.Get("/ws/test", chain(s.handleEcho(), s.upgradeHTTP(1024, 1024), s.connectionPool(ws.DefaultPool()))) -} diff --git a/service/user/user.go b/service/user/user.go new file mode 100644 index 00000000..8deae201 --- /dev/null +++ b/service/user/user.go @@ -0,0 +1,65 @@ +package user + +import ( + "errors" + "log" + "net/http" + + "github.com/go-chi/chi/middleware" + "github.com/go-chi/chi/v5" + + h "github.com/hyphengolang/prelude/http" + + "github.com/rog-golang-buddies/rmx/internal/suid" +) + +type contextKey string + +func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } + +var ( + ErrNoCookie = errors.New("api: cookie not found") + ErrSessionNotFound = errors.New("api: session not found") + ErrSessionExists = errors.New("api: session already exists") +) + +type User struct { + ID suid.SUID `json:"id"` + Name string `json:"name,omitempty"` + /* More fields can belong here */ +} + +type Service struct { + r chi.Router + l *log.Logger +} + +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } + +func New(r chi.Router) *Service { + s := &Service{r, log.Default()} + s.routes() + return s +} + +func DefaultService() *Service { + s := &Service{chi.NewMux(), log.Default()} + s.routes() + return s +} + +func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { + h.Respond(w, r, data, status) +} + +func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { + return h.Decode(w, r, data) +} + +func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { + return suid.ParseString(chi.URLParam(r, "id")) +} + +func (s *Service) routes() { + s.r.Use(middleware.Logger) +} From 3d2656ecafb6c8e3b4e06918c5cb71a30ba62ab7 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 30 Sep 2022 19:23:16 +0100 Subject: [PATCH 085/246] ping endpoint for user.Service --- service/jam/routes.go | 2 +- service/user/routes.go | 7 +++++++ service/user/routes_test.go | 24 ++++++++++++++++++++++++ service/user/user.go | 5 +++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 service/user/routes.go create mode 100644 service/user/routes_test.go diff --git a/service/jam/routes.go b/service/jam/routes.go index 63481718..0c320718 100644 --- a/service/jam/routes.go +++ b/service/jam/routes.go @@ -37,7 +37,7 @@ func (s *Service) handleGetRoom() http.HandlerFunc { return } - // ! rename method as `Get` is nondescriptive + // TODO rename method as `Get` is nondescriptive p, err := s.c.Get(uid) if err != nil { s.respond(w, r, err, http.StatusNotFound) diff --git a/service/user/routes.go b/service/user/routes.go new file mode 100644 index 00000000..81440fcd --- /dev/null +++ b/service/user/routes.go @@ -0,0 +1,7 @@ +package user + +import "net/http" + +func (s *Service) handlePing(w http.ResponseWriter, r *http.Request) { + s.respond(w, r, nil, http.StatusNoContent) +} diff --git a/service/user/routes_test.go b/service/user/routes_test.go new file mode 100644 index 00000000..9f3f0652 --- /dev/null +++ b/service/user/routes_test.go @@ -0,0 +1,24 @@ +package user + +import ( + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" +) + +func TestRoutes(t *testing.T) { + srv := New(chi.NewMux()) + + s := httptest.NewServer(srv) + t.Cleanup(func() { s.Close() }) + + r, err := s.Client().Get(s.URL + "/api/v1/ping") + if err != nil { + t.Fatal(err) + } + + if r.StatusCode != 204 { + t.Fatalf("expected %d;got %d", 204, r.StatusCode) + } +} diff --git a/service/user/user.go b/service/user/user.go index 8deae201..1d3c2053 100644 --- a/service/user/user.go +++ b/service/user/user.go @@ -62,4 +62,9 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, func (s *Service) routes() { s.r.Use(middleware.Logger) + + s.r.Route("/api/v1", func(r chi.Router) { + // health + r.Get("/ping", s.handlePing) + }) } From ac2660f538b6134e4f1dc56a657c0d8741acc13e Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 30 Sep 2022 19:44:13 +0100 Subject: [PATCH 086/246] dummy paths for user.Service --- internal/rmx.go | 6 ++++ service/jam/jam.go | 14 ++++++--- service/jam/routes.go | 71 ++++++++++-------------------------------- service/routes_test.go | 27 ---------------- service/service.go | 14 +++++---- service/user/routes.go | 12 +++++-- service/user/user.go | 20 +++++++----- 7 files changed, 62 insertions(+), 102 deletions(-) delete mode 100644 service/routes_test.go diff --git a/internal/rmx.go b/internal/rmx.go index 3f749836..76218feb 100644 --- a/internal/rmx.go +++ b/internal/rmx.go @@ -70,3 +70,9 @@ func (t *MessageTyp) UnmarshalJSON(b []byte) error { func (t MessageTyp) MarshalJSON() ([]byte, error) { return []byte(`"` + t.String() + `"`), nil } + +type JamRepo interface { +} + +type UserRepo interface { +} diff --git a/service/jam/jam.go b/service/jam/jam.go index 01d84b16..4c1fc847 100644 --- a/service/jam/jam.go +++ b/service/jam/jam.go @@ -56,7 +56,7 @@ type Service struct { c *ws.Client } -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } +func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } func New(r chi.Router) *Service { s := &Service{r, log.Default(), ws.DefaultClient} @@ -70,19 +70,19 @@ func DefaultService() *Service { return s } -func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { +func (s Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { h.Respond(w, r, data, status) } -func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { +func (s Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { return h.Decode(w, r, data) } -func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { +func (s Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { return suid.ParseString(chi.URLParam(r, "id")) } -func (s *Service) routes() { +func (s Service) routes() { s.r.Use(middleware.Logger) s.r.Route("/api/v1", func(r chi.Router) { @@ -97,3 +97,7 @@ func (s *Service) routes() { // Websocket s.r.Get("/ws/jam/{id}", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.connectionPool(nil))) } + +func (s Service) handlePing(w http.ResponseWriter, r *http.Request) { + s.respond(w, r, nil, http.StatusNoContent) +} diff --git a/service/jam/routes.go b/service/jam/routes.go index 0c320718..06664a13 100644 --- a/service/jam/routes.go +++ b/service/jam/routes.go @@ -11,8 +11,6 @@ import ( "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/suid" ws "github.com/rog-golang-buddies/rmx/internal/websocket" - - t "github.com/hyphengolang/prelude/template" ) func (s *Service) handleCreateRoom() http.HandlerFunc { @@ -37,7 +35,8 @@ func (s *Service) handleGetRoom() http.HandlerFunc { return } - // TODO rename method as `Get` is nondescriptive + // FIXME possible rename + // method as `Get` is nondescriptive p, err := s.c.Get(uid) if err != nil { s.respond(w, r, err, http.StatusNotFound) @@ -53,7 +52,7 @@ func (s *Service) handleGetRoom() http.HandlerFunc { } } -func (s *Service) handleListRooms() http.HandlerFunc { +func (s Service) handleListRooms() http.HandlerFunc { type response struct { Sessions []session `json:"sessions"` } @@ -73,10 +72,6 @@ func (s *Service) handleListRooms() http.HandlerFunc { } } -func (s *Service) handlePing(w http.ResponseWriter, r *http.Request) { - s.respond(w, r, nil, http.StatusNoContent) -} - func (s *Service) connectionPool(p *ws.Pool) func(f http.HandlerFunc) http.HandlerFunc { return func(f http.HandlerFunc) http.HandlerFunc { if p != nil { @@ -104,7 +99,7 @@ func (s *Service) connectionPool(p *ws.Pool) func(f http.HandlerFunc) http.Handl } } -func (s *Service) upgradeHTTP(readBuf, writeBuf int) func(f http.HandlerFunc) http.HandlerFunc { +func (s Service) upgradeHTTP(readBuf, writeBuf int) func(f http.HandlerFunc) http.HandlerFunc { return func(f http.HandlerFunc) http.HandlerFunc { u := &websocket.Upgrader{ ReadBufferSize: readBuf, @@ -131,7 +126,10 @@ func (s *Service) upgradeHTTP(readBuf, writeBuf int) func(f http.HandlerFunc) ht } } -func (s *Service) handleP2PComms() http.HandlerFunc { +func (s Service) handleP2PComms() http.HandlerFunc { + // FIXME we will change this as I know this hasn't been + // was just my way of getting things working, not yet + // full agreement with this. type response[T any] struct { Typ rmx.MessageTyp `json:"type"` Payload T `json:"payload"` @@ -151,7 +149,7 @@ func (s *Service) handleP2PComms() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { c := r.Context().Value(upgradeKey).(*ws.Conn) defer func() { - // ! send error when Leaving session pool + // FIXME send error when Leaving session pool c.SendMessage(response[leave]{ Typ: rmx.Leave, Payload: leave{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, @@ -168,9 +166,9 @@ func (s *Service) handleP2PComms() http.HandlerFunc { return } - // ? could the API be adjusted such that - // ? this for-loop only needs to read and - // ? never touch the code for writing + // TODO could the API be adjusted such that + // this for-loop only needs to read and + // never touch the code for writing for { var msg response[json.RawMessage] if err := c.ReadJSON(&msg); err != nil { @@ -178,8 +176,8 @@ func (s *Service) handleP2PComms() http.HandlerFunc { return } - // * here the message will be passed off to a different handler - // * via a go routine* + // TODO here the message will be passed off to a different handler + // via a go routine* if err := c.SendMessage(response[int]{Typ: rmx.Message, Payload: 10}); err != nil { s.l.Println(err) return @@ -188,7 +186,8 @@ func (s *Service) handleP2PComms() http.HandlerFunc { } } -func (s *Service) handleEcho() http.HandlerFunc { +/* TODO only here due to testing but will leave out for now +func (s Service) handleEcho() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { c := r.Context().Value(upgradeKey).(*ws.Conn) defer func() { @@ -221,40 +220,4 @@ func (s *Service) handleEcho() http.HandlerFunc { } } } - -// ! to be discarded - -func (s *Service) indexHTML(path string) http.HandlerFunc { - render, err := t.Render(path) - if err != nil { - panic(err) - } - - return func(w http.ResponseWriter, r *http.Request) { - render(w, r, nil) - } -} - -func (s *Service) jamSessionHTML(path string) http.HandlerFunc { - render, err := t.Render(path) - if err != nil { - panic(err) - } - - // ! I should be rendering a 404 page if there is an error - // ! in this layer, but for an MVC this will do - return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r) - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - if _, err = s.c.Get(uid); err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - render(w, r, nil) - } -} +*/ diff --git a/service/routes_test.go b/service/routes_test.go deleted file mode 100644 index 995e3367..00000000 --- a/service/routes_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package service - -import ( - "net/http/httptest" - "testing" - - "github.com/go-chi/chi/v5" - // "github.com/rog-golang-buddies/rmx/internal/websocket" -) - -func TestRoutes(t *testing.T) { - srv := New(chi.NewMux()) - - // srv.r.Get("/ws/echo", chain(srv.handleEcho(),srv.upgradeHTTP(1024,1024),srv.connectionPool(websocket.DefaultPool()))) - - s := httptest.NewServer(srv) - t.Cleanup(func() { s.Close() }) - - r, err := s.Client().Get(s.URL + "/ping") - if err != nil { - t.Fatal(err) - } - - if r.StatusCode != 204 { - t.Fatalf("expected %d;got %d", 204, r.StatusCode) - } -} diff --git a/service/service.go b/service/service.go index b4bd9e74..27860a2d 100644 --- a/service/service.go +++ b/service/service.go @@ -5,20 +5,22 @@ import ( "net/http" "github.com/go-chi/chi/v5" - - h "github.com/hyphengolang/prelude/http" ) -func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } - type Service struct { - r chi.Router + m chi.Router l *log.Logger } -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } func New(r chi.Router) *Service { s := &Service{r, log.Default()} return s } + +func Default() *Service { + s := &Service{chi.NewMux(), log.Default()} + // + return s +} diff --git a/service/user/routes.go b/service/user/routes.go index 81440fcd..e1989443 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -2,6 +2,14 @@ package user import "net/http" -func (s *Service) handlePing(w http.ResponseWriter, r *http.Request) { - s.respond(w, r, nil, http.StatusNoContent) +func (s Service) handleUserSignUp() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + s.respond(w, r, nil, http.StatusNotImplemented) + } +} + +func (s Service) handleUserLogin() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + s.respond(w, r, nil, http.StatusNotImplemented) + } } diff --git a/service/user/user.go b/service/user/user.go index 1d3c2053..9e101923 100644 --- a/service/user/user.go +++ b/service/user/user.go @@ -5,7 +5,6 @@ import ( "log" "net/http" - "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" h "github.com/hyphengolang/prelude/http" @@ -34,7 +33,7 @@ type Service struct { l *log.Logger } -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } +func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } func New(r chi.Router) *Service { s := &Service{r, log.Default()} @@ -48,23 +47,28 @@ func DefaultService() *Service { return s } -func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { +func (s Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { h.Respond(w, r, data, status) } -func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { +func (s Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { return h.Decode(w, r, data) } -func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { +func (s Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { return suid.ParseString(chi.URLParam(r, "id")) } -func (s *Service) routes() { - s.r.Use(middleware.Logger) - +func (s Service) routes() { s.r.Route("/api/v1", func(r chi.Router) { + r.Get("/tba", s.handleUserLogin()) + r.Post("/tba", s.handleUserSignUp()) + // health r.Get("/ping", s.handlePing) }) } + +func (s Service) handlePing(w http.ResponseWriter, r *http.Request) { + s.respond(w, r, nil, http.StatusNoContent) +} From 564c5563546afe6fc0017ec8df422d60fcffcf21 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 30 Sep 2022 19:58:43 +0100 Subject: [PATCH 087/246] unit & integration tests for services (handlePing) --- service/jam/jam.go | 10 +++++----- service/jam/routes_test.go | 4 ++-- service/service.go | 13 ++++++++++--- service/service_test.go | 33 +++++++++++++++++++++++++++++++++ service/user/routes_test.go | 4 ++-- service/user/user.go | 4 ++-- 6 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 service/service_test.go diff --git a/service/jam/jam.go b/service/jam/jam.go index 4c1fc847..78e1ffd5 100644 --- a/service/jam/jam.go +++ b/service/jam/jam.go @@ -58,7 +58,7 @@ type Service struct { func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } -func New(r chi.Router) *Service { +func NewService(r chi.Router) *Service { s := &Service{r, log.Default(), ws.DefaultClient} s.routes() return s @@ -85,10 +85,10 @@ func (s Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, e func (s Service) routes() { s.r.Use(middleware.Logger) - s.r.Route("/api/v1", func(r chi.Router) { - r.Get("/jam", s.handleListRooms()) - r.Post("/jam", s.handleCreateRoom()) - r.Get("/jam/{id}", s.handleGetRoom()) + s.r.Route("/api/v1/jam", func(r chi.Router) { + r.Get("/", s.handleListRooms()) + r.Post("/", s.handleCreateRoom()) + r.Get("/{id}", s.handleGetRoom()) // health r.Get("/ping", s.handlePing) diff --git a/service/jam/routes_test.go b/service/jam/routes_test.go index eded74cd..3d81ce97 100644 --- a/service/jam/routes_test.go +++ b/service/jam/routes_test.go @@ -9,14 +9,14 @@ import ( ) func TestRoutes(t *testing.T) { - srv := New(chi.NewMux()) + srv := NewService(chi.NewMux()) // srv.r.Get("/ws/echo", chain(srv.handleEcho(),srv.upgradeHTTP(1024,1024),srv.connectionPool(websocket.DefaultPool()))) s := httptest.NewServer(srv) t.Cleanup(func() { s.Close() }) - r, err := s.Client().Get(s.URL + "/api/v1/ping/") + r, err := s.Client().Get(s.URL + "/api/v1/jam/ping") if err != nil { t.Fatal(err) } diff --git a/service/service.go b/service/service.go index 27860a2d..9a2cd926 100644 --- a/service/service.go +++ b/service/service.go @@ -4,7 +4,10 @@ import ( "log" "net/http" + "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" + "github.com/rog-golang-buddies/rmx/service/jam" + "github.com/rog-golang-buddies/rmx/service/user" ) type Service struct { @@ -16,11 +19,15 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeH func New(r chi.Router) *Service { s := &Service{r, log.Default()} + s.m.Use(middleware.Logger) + + // NOTE unsure how much is gained using a goroutine + // will have to investigate + go jam.NewService(s.m) + go user.NewService(s.m) return s } func Default() *Service { - s := &Service{chi.NewMux(), log.Default()} - // - return s + return New(chi.NewMux()) } diff --git a/service/service_test.go b/service/service_test.go new file mode 100644 index 00000000..89ec7828 --- /dev/null +++ b/service/service_test.go @@ -0,0 +1,33 @@ +package service + +import ( + "net/http/httptest" + "testing" +) + +func TestIntegration(t *testing.T) { + srv := Default() + + s := httptest.NewServer(srv) + t.Cleanup(func() { s.Close() }) + + // jam service + r, err := s.Client().Get(s.URL + "/api/v1/jam/ping") + if err != nil { + t.Fatal(err) + } + + if r.StatusCode != 204 { + t.Fatalf("expected %d;got %d", 204, r.StatusCode) + } + + // user service + r, err = s.Client().Get(s.URL + "/api/v1/user/ping") + if err != nil { + t.Fatal(err) + } + + if r.StatusCode != 204 { + t.Fatalf("expected %d;got %d", 204, r.StatusCode) + } +} diff --git a/service/user/routes_test.go b/service/user/routes_test.go index 9f3f0652..e3818da0 100644 --- a/service/user/routes_test.go +++ b/service/user/routes_test.go @@ -8,12 +8,12 @@ import ( ) func TestRoutes(t *testing.T) { - srv := New(chi.NewMux()) + srv := NewService(chi.NewMux()) s := httptest.NewServer(srv) t.Cleanup(func() { s.Close() }) - r, err := s.Client().Get(s.URL + "/api/v1/ping") + r, err := s.Client().Get(s.URL + "/api/v1/user/ping") if err != nil { t.Fatal(err) } diff --git a/service/user/user.go b/service/user/user.go index 9e101923..27482600 100644 --- a/service/user/user.go +++ b/service/user/user.go @@ -35,7 +35,7 @@ type Service struct { func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } -func New(r chi.Router) *Service { +func NewService(r chi.Router) *Service { s := &Service{r, log.Default()} s.routes() return s @@ -60,7 +60,7 @@ func (s Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, e } func (s Service) routes() { - s.r.Route("/api/v1", func(r chi.Router) { + s.r.Route("/api/v1/user", func(r chi.Router) { r.Get("/tba", s.handleUserLogin()) r.Post("/tba", s.handleUserSignUp()) From 6f5845d59b2d56ada86e15d15c83a34b2a85d3b8 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 30 Sep 2022 20:02:52 +0100 Subject: [PATCH 088/246] (s Service) -> (s *Service) --- service/jam/jam.go | 12 ++++++------ service/user/routes.go | 4 ++-- service/user/user.go | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/service/jam/jam.go b/service/jam/jam.go index 78e1ffd5..95327760 100644 --- a/service/jam/jam.go +++ b/service/jam/jam.go @@ -56,7 +56,7 @@ type Service struct { c *ws.Client } -func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } func NewService(r chi.Router) *Service { s := &Service{r, log.Default(), ws.DefaultClient} @@ -70,19 +70,19 @@ func DefaultService() *Service { return s } -func (s Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { +func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { h.Respond(w, r, data, status) } -func (s Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { +func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { return h.Decode(w, r, data) } -func (s Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { +func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { return suid.ParseString(chi.URLParam(r, "id")) } -func (s Service) routes() { +func (s *Service) routes() { s.r.Use(middleware.Logger) s.r.Route("/api/v1/jam", func(r chi.Router) { @@ -98,6 +98,6 @@ func (s Service) routes() { s.r.Get("/ws/jam/{id}", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.connectionPool(nil))) } -func (s Service) handlePing(w http.ResponseWriter, r *http.Request) { +func (s *Service) handlePing(w http.ResponseWriter, r *http.Request) { s.respond(w, r, nil, http.StatusNoContent) } diff --git a/service/user/routes.go b/service/user/routes.go index e1989443..92089411 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -2,13 +2,13 @@ package user import "net/http" -func (s Service) handleUserSignUp() http.HandlerFunc { +func (s *Service) handleUserSignUp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { s.respond(w, r, nil, http.StatusNotImplemented) } } -func (s Service) handleUserLogin() http.HandlerFunc { +func (s *Service) handleUserLogin() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { s.respond(w, r, nil, http.StatusNotImplemented) } diff --git a/service/user/user.go b/service/user/user.go index 27482600..ef33a552 100644 --- a/service/user/user.go +++ b/service/user/user.go @@ -33,7 +33,7 @@ type Service struct { l *log.Logger } -func (s Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } func NewService(r chi.Router) *Service { s := &Service{r, log.Default()} @@ -47,19 +47,19 @@ func DefaultService() *Service { return s } -func (s Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { +func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { h.Respond(w, r, data, status) } -func (s Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { +func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { return h.Decode(w, r, data) } -func (s Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { +func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { return suid.ParseString(chi.URLParam(r, "id")) } -func (s Service) routes() { +func (s *Service) routes() { s.r.Route("/api/v1/user", func(r chi.Router) { r.Get("/tba", s.handleUserLogin()) r.Post("/tba", s.handleUserSignUp()) @@ -69,6 +69,6 @@ func (s Service) routes() { }) } -func (s Service) handlePing(w http.ResponseWriter, r *http.Request) { +func (s *Service) handlePing(w http.ResponseWriter, r *http.Request) { s.respond(w, r, nil, http.StatusNoContent) } From 108e8b78697d20f8cc4426c58ef986ca7b1bd2e5 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 30 Sep 2022 21:03:06 +0100 Subject: [PATCH 089/246] mock user repo --- cmd/main.go | 4 +- internal/rmx.go | 22 +++++++++ service/jam/{jam.go => service.go} | 14 +++--- service/service.go | 11 +++-- service/user/routes_test.go | 4 +- service/user/{user.go => service.go} | 17 ++++--- test/mock/repo.go | 74 ++++++++++++++++++++++++++++ 7 files changed, 123 insertions(+), 23 deletions(-) rename service/jam/{jam.go => service.go} (88%) rename service/user/{user.go => service.go} (79%) create mode 100644 test/mock/repo.go diff --git a/cmd/main.go b/cmd/main.go index 50a219e0..514259ea 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -14,8 +14,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/rs/cors" "golang.org/x/sync/errgroup" - - "github.com/rog-golang-buddies/rmx/service" ) // var ( @@ -42,7 +40,7 @@ func run(cfg *Config) error { srv := http.Server{ Addr: ":" + strconv.Itoa(cfg.Port), - Handler: cors.New(c).Handler(service.New(chi.NewMux())), + Handler: cors.New(c).Handler(chi.NewMux()), // max time to read request from the client ReadTimeout: 10 * time.Second, // max time to write response to the client diff --git a/internal/rmx.go b/internal/rmx.go index 76218feb..0a13692c 100644 --- a/internal/rmx.go +++ b/internal/rmx.go @@ -2,6 +2,8 @@ package internal import ( "errors" + + "github.com/rog-golang-buddies/rmx/internal/suid" ) var ErrTodo = errors.New("rmx: not yet implemented") @@ -71,8 +73,28 @@ func (t MessageTyp) MarshalJSON() ([]byte, error) { return []byte(`"` + t.String() + `"`), nil } +// TODO +type Email string + type JamRepo interface { } +type User struct { + ID suid.UUID + Email Email +} + type UserRepo interface { + RUserRepo + WUserRepo +} + +type RUserRepo interface { + Lookup(id suid.UUID) (*User, error) + LookupEmail(email Email) (*User, error) + ListAll() ([]*User, error) +} + +type WUserRepo interface { + SignUp(u *User) error } diff --git a/service/jam/jam.go b/service/jam/service.go similarity index 88% rename from service/jam/jam.go rename to service/jam/service.go index 95327760..37315493 100644 --- a/service/jam/jam.go +++ b/service/jam/service.go @@ -50,13 +50,13 @@ type User struct { } type Service struct { - r chi.Router + m chi.Router l *log.Logger c *ws.Client } -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } func NewService(r chi.Router) *Service { s := &Service{r, log.Default(), ws.DefaultClient} @@ -79,23 +79,23 @@ func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{ } func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { - return suid.ParseString(chi.URLParam(r, "id")) + return suid.ParseString(chi.URLParam(r, "uuid")) } func (s *Service) routes() { - s.r.Use(middleware.Logger) + s.m.Use(middleware.Logger) - s.r.Route("/api/v1/jam", func(r chi.Router) { + s.m.Route("/api/v1/jam", func(r chi.Router) { r.Get("/", s.handleListRooms()) r.Post("/", s.handleCreateRoom()) - r.Get("/{id}", s.handleGetRoom()) + r.Get("/{uuid}", s.handleGetRoom()) // health r.Get("/ping", s.handlePing) }) // Websocket - s.r.Get("/ws/jam/{id}", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.connectionPool(nil))) + s.m.Get("/ws/jam/{id}", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.connectionPool(nil))) } func (s *Service) handlePing(w http.ResponseWriter, r *http.Request) { diff --git a/service/service.go b/service/service.go index 9a2cd926..7dcd2b8b 100644 --- a/service/service.go +++ b/service/service.go @@ -6,8 +6,11 @@ import ( "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" + "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/service/jam" "github.com/rog-golang-buddies/rmx/service/user" + + "github.com/rog-golang-buddies/rmx/test/mock" ) type Service struct { @@ -17,17 +20,17 @@ type Service struct { func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func New(r chi.Router) *Service { - s := &Service{r, log.Default()} +func New(m chi.Router, ur internal.UserRepo) *Service { + s := &Service{m, log.Default()} s.m.Use(middleware.Logger) // NOTE unsure how much is gained using a goroutine // will have to investigate go jam.NewService(s.m) - go user.NewService(s.m) + go user.NewService(s.m, ur) return s } func Default() *Service { - return New(chi.NewMux()) + return New(chi.NewMux(), mock.UserRepo()) } diff --git a/service/user/routes_test.go b/service/user/routes_test.go index e3818da0..421c872d 100644 --- a/service/user/routes_test.go +++ b/service/user/routes_test.go @@ -3,12 +3,10 @@ package user import ( "net/http/httptest" "testing" - - "github.com/go-chi/chi/v5" ) func TestRoutes(t *testing.T) { - srv := NewService(chi.NewMux()) + srv := DefaultService() s := httptest.NewServer(srv) t.Cleanup(func() { s.Close() }) diff --git a/service/user/user.go b/service/user/service.go similarity index 79% rename from service/user/user.go rename to service/user/service.go index ef33a552..ebf003a3 100644 --- a/service/user/user.go +++ b/service/user/service.go @@ -9,7 +9,9 @@ import ( h "github.com/hyphengolang/prelude/http" + "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/suid" + "github.com/rog-golang-buddies/rmx/test/mock" ) type contextKey string @@ -29,20 +31,23 @@ type User struct { } type Service struct { - r chi.Router + m chi.Router + r internal.RUserRepo + l *log.Logger } -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.r.ServeHTTP(w, r) } +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func NewService(r chi.Router) *Service { - s := &Service{r, log.Default()} +func NewService(m chi.Router, r internal.RUserRepo) *Service { + s := &Service{m, r, log.Default()} s.routes() return s } func DefaultService() *Service { - s := &Service{chi.NewMux(), log.Default()} + + s := &Service{chi.NewMux(), mock.UserRepo(), log.Default()} s.routes() return s } @@ -60,7 +65,7 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, } func (s *Service) routes() { - s.r.Route("/api/v1/user", func(r chi.Router) { + s.m.Route("/api/v1/user", func(r chi.Router) { r.Get("/tba", s.handleUserLogin()) r.Post("/tba", s.handleUserSignUp()) diff --git a/test/mock/repo.go b/test/mock/repo.go new file mode 100644 index 00000000..82b5eb6a --- /dev/null +++ b/test/mock/repo.go @@ -0,0 +1,74 @@ +package mock + +import ( + "errors" + "sync" + + "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/suid" + "golang.org/x/exp/maps" +) + +var ( + errTodo = errors.New("not yet implemented") + errNotFound = errors.New("user not found") + errExists = errors.New("user already exists") +) + +type userRepo struct { + mu sync.Mutex + us map[suid.UUID]*internal.User +} + +func UserRepo() *userRepo { + r := &userRepo{ + us: map[suid.UUID]*internal.User{}, + } + return r +} + +func (r *userRepo) ListAll() ([]*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() + + return maps.Values(r.us), errTodo +} + +func (r *userRepo) Lookup(uid suid.UUID) (*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() + + for _, u := range r.us { + if u.ID == uid { + return u, nil + } + } + + return nil, errNotFound +} + +func (r *userRepo) LookupEmail(email internal.Email) (*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() + + for _, u := range r.us { + if u.Email == email { + return u, nil + } + } + + return nil, errTodo +} + +func (r *userRepo) SignUp(u *internal.User) error { + r.mu.Lock() + defer r.mu.Unlock() + + if _, found := r.us[u.ID]; found { + return errExists + } else { + r.us[u.ID] = u + } + + return nil +} From bbe067574cd47e1d8ba4623ed11fe10905f6376e Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 30 Sep 2022 21:03:35 +0100 Subject: [PATCH 090/246] remove errTodo --- test/mock/repo.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/mock/repo.go b/test/mock/repo.go index 82b5eb6a..fe5bc6fe 100644 --- a/test/mock/repo.go +++ b/test/mock/repo.go @@ -31,7 +31,7 @@ func (r *userRepo) ListAll() ([]*internal.User, error) { r.mu.Lock() defer r.mu.Unlock() - return maps.Values(r.us), errTodo + return maps.Values(r.us), nil } func (r *userRepo) Lookup(uid suid.UUID) (*internal.User, error) { @@ -57,7 +57,7 @@ func (r *userRepo) LookupEmail(email internal.Email) (*internal.User, error) { } } - return nil, errTodo + return nil, errNotFound } func (r *userRepo) SignUp(u *internal.User) error { From 99751d74ba80b89f4e1b4cf11d74d4e39b29286b Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 30 Sep 2022 21:22:18 +0100 Subject: [PATCH 091/246] Email, Password(Hash) types added --- go.mod | 2 + go.sum | 4 + internal/internal.go | 150 +++++++++++++++++++++++++++++++++++ internal/internal_test.go | 68 ++++++++++++++++ internal/rmx.go | 100 ----------------------- internal/websocket/client.go | 2 +- internal/websocket/conn.go | 4 +- service/jam/routes.go | 4 +- sqlc.yaml | 28 +++---- 9 files changed, 243 insertions(+), 119 deletions(-) create mode 100644 internal/internal.go create mode 100644 internal/internal_test.go delete mode 100644 internal/rmx.go diff --git a/go.mod b/go.mod index a67ffdf5..c569e248 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/lithammer/shortuuid/v4 v4.0.0 github.com/rs/cors v1.8.2 github.com/urfave/cli/v2 v2.16.3 + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e golang.org/x/exp v0.0.0-20220921164117-439092de6870 golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 golang.org/x/term v0.0.0-20220919170432-7a66f970e087 @@ -33,6 +34,7 @@ require ( github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/wagslane/go-password-validator v0.3.0 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect golang.org/x/text v0.3.7 // indirect diff --git a/go.sum b/go.sum index 80e92bf1..0ce91064 100644 --- a/go.sum +++ b/go.sum @@ -59,8 +59,12 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk= github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= +github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= +github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20220921164117-439092de6870 h1:j8b6j9gzSigH28O5SjSpQSSh9lFd6f5D/q0aHjNTulc= golang.org/x/exp v0.0.0-20220921164117-439092de6870/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc= diff --git a/internal/internal.go b/internal/internal.go new file mode 100644 index 00000000..8faeea99 --- /dev/null +++ b/internal/internal.go @@ -0,0 +1,150 @@ +package internal + +import ( + "errors" + "regexp" + "strings" + + "github.com/rog-golang-buddies/rmx/internal/suid" + gpv "github.com/wagslane/go-password-validator" + "golang.org/x/crypto/bcrypt" +) + +var ErrInvalidEmail = errors.New("rmx: invalid email") +var ErrNotImplemented = errors.New("rmx: not yet implemented") + +type MsgTyp int + +const ( + Unknown = iota + + Create + Delete + + Join + Leave + Message + + NoteOn + NoteOff +) + +func (t MsgTyp) String() string { + switch t { + case Create: + return "CREATE" + case Delete: + return "DELETE" + case Join: + return "JOIN" + case Leave: + return "LEAVE" + case Message: + return "MESSAGE" + case NoteOn: + return "NOTE_ON" + case NoteOff: + return "NOTE_OFF" + + default: + return "UNKNOWN" + } +} + +func (t *MsgTyp) UnmarshalJSON(b []byte) error { + switch s := string(b[1 : len(b)-1]); s { + case "CREATE": + *t = Create + case "DELETE": + *t = Delete + case "JOIN": + *t = Join + case "LEAVE": + *t = Leave + case "MESSAGE": + *t = Message + case "NOTE_ON": + *t = NoteOn + case "NOTE_OFF": + *t = NoteOff + default: + *t = Unknown + } + + return nil +} + +func (t MsgTyp) MarshalJSON() ([]byte, error) { + var sb strings.Builder + sb.WriteRune('"') + sb.WriteString(t.String()) + sb.WriteRune('"') + return []byte(sb.String()), nil +} + +var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9.!#$%&’*+/=?^_\x60{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$`) + +type Email string + +func (e *Email) UnmarshalJSON(b []byte) error { + if s := b[1 : len(b)-1]; emailRegex.Match(s) { + *e = Email(s) + return nil + } + + return ErrInvalidEmail +} + +const minEntropy float64 = 10.0 + +type Password string + +func (p *Password) UnmarshalJSON(b []byte) error { + *p = Password(b[1 : len(b)-1]) + return gpv.Validate(string(*p), minEntropy) +} + +func (p Password) MarshalJSON() (b []byte, err error) { + var sb strings.Builder + sb.WriteRune('"') + sb.WriteString(string(p)) + sb.WriteRune('"') + return []byte(sb.String()), nil +} + +func (p Password) Hash() (PasswordHash, error) { return newPasswordHash(p) } + +type PasswordHash []byte + +func newPasswordHash(pw Password) (PasswordHash, error) { + return bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) +} + +func (pw PasswordHash) Compare(cmp Password) error { + return bcrypt.CompareHashAndPassword(pw, []byte(cmp)) +} + +type JamRepo interface { +} + +type User struct { + ID suid.UUID + Email Email + Username string + Password PasswordHash +} + +type UserRepo interface { + RUserRepo + WUserRepo +} + +type RUserRepo interface { + Lookup(id suid.UUID) (*User, error) + LookupEmail(email Email) (*User, error) + ListAll() ([]*User, error) +} + +type WUserRepo interface { + SignUp(u *User) error +} diff --git a/internal/internal_test.go b/internal/internal_test.go new file mode 100644 index 00000000..071fffda --- /dev/null +++ b/internal/internal_test.go @@ -0,0 +1,68 @@ +package internal + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestPasswordType(t *testing.T) { + t.Parallel() + + data := `"thispasswordiscomplex"` + + var pw Password + err := json.NewDecoder(strings.NewReader(data)).Decode(&pw) + if err != nil { + t.Fatal(err) + } + + if exp := data[1 : len(data)-1]; pw != Password(exp) { + t.Errorf(`expected %s got %s`, exp, pw) + } + + hash, err := pw.Hash() + if err != nil { + t.Fatal(err) + } + + err = hash.Compare(pw) + if err != nil { + t.Fatal(err) + } + + err = hash.Compare("1234") + if err == nil { + t.Fatal("expected an error") + } +} + +func TestEmailType(t *testing.T) { + t.Parallel() + + data := `"anon@gmail.com"` + + var e Email + err := json.NewDecoder(strings.NewReader(data)).Decode(&e) + if err != nil { + t.Fatal(err) + } + + var sb strings.Builder + err = json.NewEncoder(&sb).Encode(e) + if err != nil { + t.Fatal(err) + } + + if s := strings.TrimSpace(sb.String()); s[1:len(s)-1] != string(e) { + t.Fatalf("expected %s got %s", e, s) + } + + // should return an error + data = `"anon@.gmail.com"` + + err = json.NewDecoder(strings.NewReader(data)).Decode(&e) + if err == nil { + t.Errorf("expected %v", ErrInvalidEmail) + } +} diff --git a/internal/rmx.go b/internal/rmx.go deleted file mode 100644 index 0a13692c..00000000 --- a/internal/rmx.go +++ /dev/null @@ -1,100 +0,0 @@ -package internal - -import ( - "errors" - - "github.com/rog-golang-buddies/rmx/internal/suid" -) - -var ErrTodo = errors.New("rmx: not yet implemented") - -type MessageTyp int - -const ( - Unknown = iota - - Create - Delete - - Join - Leave - Message - - NoteOn - NoteOff -) - -func (t MessageTyp) String() string { - switch t { - case Create: - return "CREATE" - case Delete: - return "DELETE" - case Join: - return "JOIN" - case Leave: - return "LEAVE" - case Message: - return "MESSAGE" - case NoteOn: - return "NOTE_ON" - case NoteOff: - return "NOTE_OFF" - - default: - return "UNKNOWN" - } -} - -func (t *MessageTyp) UnmarshalJSON(b []byte) error { - switch s := string(b[1 : len(b)-1]); s { - case "CREATE": - *t = Create - case "DELETE": - *t = Delete - case "JOIN": - *t = Join - case "LEAVE": - *t = Leave - case "MESSAGE": - *t = Message - case "NOTE_ON": - *t = NoteOn - case "NOTE_OFF": - *t = NoteOff - default: - *t = Unknown - } - - return nil -} - -func (t MessageTyp) MarshalJSON() ([]byte, error) { - return []byte(`"` + t.String() + `"`), nil -} - -// TODO -type Email string - -type JamRepo interface { -} - -type User struct { - ID suid.UUID - Email Email -} - -type UserRepo interface { - RUserRepo - WUserRepo -} - -type RUserRepo interface { - Lookup(id suid.UUID) (*User, error) - LookupEmail(email Email) (*User, error) - ListAll() ([]*User, error) -} - -type WUserRepo interface { - SignUp(u *User) error -} diff --git a/internal/websocket/client.go b/internal/websocket/client.go index fa3106a1..2b89359d 100644 --- a/internal/websocket/client.go +++ b/internal/websocket/client.go @@ -29,7 +29,7 @@ func NewClient() *Client { func (c *Client) Size() int { return len(c.ps) } func (c *Client) Close() error { - return rmx.ErrTodo + return rmx.ErrNotImplemented } func (c *Client) NewPool(maxCount int) (suid.UUID, error) { diff --git a/internal/websocket/conn.go b/internal/websocket/conn.go index 5aaf016a..7085b70c 100644 --- a/internal/websocket/conn.go +++ b/internal/websocket/conn.go @@ -30,9 +30,9 @@ func (c Conn) SendMessage(v any) error { return nil } -func (c Conn) SendMessage2(typ rmx.MessageTyp, data any) error { +func (c Conn) SendMessage2(typ rmx.MsgTyp, data any) error { v := struct { - Typ rmx.MessageTyp + Typ rmx.MsgTyp }{} c.p.msgs <- v return nil diff --git a/service/jam/routes.go b/service/jam/routes.go index 06664a13..99ad9874 100644 --- a/service/jam/routes.go +++ b/service/jam/routes.go @@ -131,8 +131,8 @@ func (s Service) handleP2PComms() http.HandlerFunc { // was just my way of getting things working, not yet // full agreement with this. type response[T any] struct { - Typ rmx.MessageTyp `json:"type"` - Payload T `json:"payload"` + Typ rmx.MsgTyp `json:"type"` + Payload T `json:"payload"` } type join struct { diff --git a/sqlc.yaml b/sqlc.yaml index 351737fc..aa3f21da 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -1,15 +1,15 @@ -version: '2' +version: "2" sql: - - schema: 'internal/db/schema/user_schema.sql' - queries: 'internal/db/schema/user_query.sql' - engine: 'mysql' - gen: - go: - package: 'user' - out: 'internal/db/user' - emit_db_tags: true - emit_prepared_queries: true - emit_empty_slices: true - emit_params_struct_pointers: true - emit_json_tags: true - json_tags_case_style: 'camel' + - schema: "idb/schema/user_schema.sql" + queries: "db/schema/user_query.sql" + engine: "mysql" + gen: + go: + package: "user" + out: "db/user" + emit_db_tags: true + emit_prepared_queries: true + emit_empty_slices: true + emit_params_struct_pointers: true + emit_json_tags: true + json_tags_case_style: "camel" From 1eca293c0807c9223cde91c96ea5786a3e0fad84 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 30 Sep 2022 21:22:59 +0100 Subject: [PATCH 092/246] move repo/user struct defs to the top --- internal/internal.go | 50 ++++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/internal/internal.go b/internal/internal.go index 8faeea99..9a999d71 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -13,6 +13,31 @@ import ( var ErrInvalidEmail = errors.New("rmx: invalid email") var ErrNotImplemented = errors.New("rmx: not yet implemented") +type JamRepo interface { +} + +type User struct { + ID suid.UUID + Email Email + Username string + Password PasswordHash +} + +type UserRepo interface { + RUserRepo + WUserRepo +} + +type RUserRepo interface { + Lookup(id suid.UUID) (*User, error) + LookupEmail(email Email) (*User, error) + ListAll() ([]*User, error) +} + +type WUserRepo interface { + SignUp(u *User) error +} + type MsgTyp int const ( @@ -123,28 +148,3 @@ func newPasswordHash(pw Password) (PasswordHash, error) { func (pw PasswordHash) Compare(cmp Password) error { return bcrypt.CompareHashAndPassword(pw, []byte(cmp)) } - -type JamRepo interface { -} - -type User struct { - ID suid.UUID - Email Email - Username string - Password PasswordHash -} - -type UserRepo interface { - RUserRepo - WUserRepo -} - -type RUserRepo interface { - Lookup(id suid.UUID) (*User, error) - LookupEmail(email Email) (*User, error) - ListAll() ([]*User, error) -} - -type WUserRepo interface { - SignUp(u *User) error -} From 97e0558c815ac5becf09fb5788197972155b4efc Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 30 Sep 2022 21:48:40 +0100 Subject: [PATCH 093/246] simple signup example, bound to change --- internal/internal.go | 2 +- service/user/routes.go | 48 +++++++++++++++++++++++++++++++++++-- service/user/routes_test.go | 17 ++++++++++--- service/user/service.go | 28 ++++++++++------------ test/mock/repo.go | 4 ++-- 5 files changed, 76 insertions(+), 23 deletions(-) diff --git a/internal/internal.go b/internal/internal.go index 9a999d71..df8ae40f 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -35,7 +35,7 @@ type RUserRepo interface { } type WUserRepo interface { - SignUp(u *User) error + SignUp(u User) error } type MsgTyp int diff --git a/service/user/routes.go b/service/user/routes.go index 92089411..e43c9564 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -1,10 +1,32 @@ package user -import "net/http" +import ( + "net/http" + + "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/suid" +) func (s *Service) handleUserSignUp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - s.respond(w, r, nil, http.StatusNotImplemented) + var v User + if err := s.decode(w, r, &v); err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + var u internal.User + if err := v.Decode(&u); err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + if err := s.r.SignUp(u); err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + s.created(w, r, u.ID.String()) } } @@ -13,3 +35,25 @@ func (s *Service) handleUserLogin() http.HandlerFunc { s.respond(w, r, nil, http.StatusNotImplemented) } } + +type User struct { + Email internal.Email `json:"email"` + Username string `json:"username"` + Password internal.Password `json:"password"` +} + +func (u User) Decode(iu *internal.User) error { + h, err := u.Password.Hash() + if err != nil { + return err + } + + *iu = internal.User{ + ID: suid.NewUUID(), + Email: u.Email, + Username: u.Username, + Password: h, + } + + return nil +} diff --git a/service/user/routes_test.go b/service/user/routes_test.go index 421c872d..5af6d41b 100644 --- a/service/user/routes_test.go +++ b/service/user/routes_test.go @@ -1,7 +1,9 @@ package user import ( + "net/http" "net/http/httptest" + "strings" "testing" ) @@ -11,12 +13,21 @@ func TestRoutes(t *testing.T) { s := httptest.NewServer(srv) t.Cleanup(func() { s.Close() }) - r, err := s.Client().Get(s.URL + "/api/v1/user/ping") + // TODO|FIXME needs to return an error if + // `password`, `email` or `username` is not present + payload := ` +{ + "username":"Test User", + "password":"difficultPassword", + "email":"user@gmail.com" +}` + + r, err := s.Client().Post(s.URL+"/api/v1/user", "application/json", strings.NewReader(payload)) if err != nil { t.Fatal(err) } - if r.StatusCode != 204 { - t.Fatalf("expected %d;got %d", 204, r.StatusCode) + if r.StatusCode != http.StatusCreated { + t.Fatalf("expected %d; got %d", http.StatusCreated, r.StatusCode) } } diff --git a/service/user/service.go b/service/user/service.go index ebf003a3..2d29e33f 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -14,32 +14,26 @@ import ( "github.com/rog-golang-buddies/rmx/test/mock" ) -type contextKey string +// type contextKey string -func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } +// func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } var ( - ErrNoCookie = errors.New("api: cookie not found") - ErrSessionNotFound = errors.New("api: session not found") - ErrSessionExists = errors.New("api: session already exists") + ErrNoCookie = errors.New("user: cookie not found") + ErrSessionNotFound = errors.New("user: session not found") + ErrSessionExists = errors.New("user: session already exists") ) -type User struct { - ID suid.SUID `json:"id"` - Name string `json:"name,omitempty"` - /* More fields can belong here */ -} - type Service struct { m chi.Router - r internal.RUserRepo + r internal.UserRepo l *log.Logger } func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func NewService(m chi.Router, r internal.RUserRepo) *Service { +func NewService(m chi.Router, r internal.UserRepo) *Service { s := &Service{m, r, log.Default()} s.routes() return s @@ -56,6 +50,10 @@ func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, stat h.Respond(w, r, data, status) } +func (s *Service) created(w http.ResponseWriter, r *http.Request, id string) { + h.Created(w, r, id) +} + func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { return h.Decode(w, r, data) } @@ -66,8 +64,8 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, func (s *Service) routes() { s.m.Route("/api/v1/user", func(r chi.Router) { - r.Get("/tba", s.handleUserLogin()) - r.Post("/tba", s.handleUserSignUp()) + r.Get("/", s.handleUserLogin()) + r.Post("/", s.handleUserSignUp()) // health r.Get("/ping", s.handlePing) diff --git a/test/mock/repo.go b/test/mock/repo.go index fe5bc6fe..6392443c 100644 --- a/test/mock/repo.go +++ b/test/mock/repo.go @@ -60,14 +60,14 @@ func (r *userRepo) LookupEmail(email internal.Email) (*internal.User, error) { return nil, errNotFound } -func (r *userRepo) SignUp(u *internal.User) error { +func (r *userRepo) SignUp(u internal.User) error { r.mu.Lock() defer r.mu.Unlock() if _, found := r.us[u.ID]; found { return errExists } else { - r.us[u.ID] = u + r.us[u.ID] = &u } return nil From edb884d523f88f9e7ab5e9b9df4cb6a4ab74815f Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 1 Oct 2022 08:52:54 +0100 Subject: [PATCH 094/246] logic for auth slowly being brought in --- service/user/auth_test.go | 14 +++ service/user/routes.go | 173 +++++++++++++++++++++++++++++++++----- service/user/service.go | 24 +++++- 3 files changed, 189 insertions(+), 22 deletions(-) create mode 100644 service/user/auth_test.go diff --git a/service/user/auth_test.go b/service/user/auth_test.go new file mode 100644 index 00000000..f09a30c9 --- /dev/null +++ b/service/user/auth_test.go @@ -0,0 +1,14 @@ +package user + +import "testing" + +var jwtKey = []byte("my_secret_key") + +var users = map[string]string{ + "user_1": "password_1", + "user_2": "password_2", +} + +func TestAuth(t *testing.T) { + +} diff --git a/service/user/routes.go b/service/user/routes.go index e43c9564..c8011f7d 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -1,22 +1,57 @@ package user import ( + "errors" "net/http" + "strings" + "time" "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/suid" ) -func (s *Service) handleUserSignUp() http.HandlerFunc { +// var refreshTokenCookieName = "RMX_DIRECT_RT" +// var refreshTokenCookiePath = "/api/v1" + +// 400 - catch all +// 401 - unauthorized +// 403 - Forbidden +// 409 - Conflict (details already exist) +// 412 - Invalid precondition +// 422 - Unprocessable + +type signup struct { + Email internal.Email `json:"email"` + Username string `json:"username"` + Password internal.Password `json:"password"` +} + +func (v signup) decode(iu *internal.User) error { + h, err := v.Password.Hash() + if err != nil { + return err + } + + *iu = internal.User{ + ID: suid.NewUUID(), + Email: v.Email, + Username: v.Username, + Password: h, + } + + return nil +} + +func (s *Service) handleRegistration() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var v User + var v signup if err := s.decode(w, r, &v); err != nil { s.respond(w, r, err, http.StatusBadRequest) return } var u internal.User - if err := v.Decode(&u); err != nil { + if err := v.decode(&u); err != nil { s.respond(w, r, err, http.StatusInternalServerError) return } @@ -30,30 +65,130 @@ func (s *Service) handleUserSignUp() http.HandlerFunc { } } -func (s *Service) handleUserLogin() http.HandlerFunc { +func (s *Service) handleLogin() http.HandlerFunc { + type login struct { + Email internal.Email `json:"email"` + Password internal.Password `json:"password"` + } + + type response struct { + IDToken string `json:"id_token"` + AccessToken string `json:"access_token"` + } + return func(w http.ResponseWriter, r *http.Request) { - s.respond(w, r, nil, http.StatusNotImplemented) + var v login + if err := s.decode(w, r, &v); err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + u, err := s.r.LookupEmail(v.Email) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + if err := u.Password.Compare(v.Password); err != nil { + s.respond(w, r, err, http.StatusUnauthorized) + return + } + + /* + Claims & Token + + Token generated by the user + */ + + cookie := &http.Cookie{ + Path: "REFRESH_TOKEN_COOKIE_PATH", + Name: "REFRESH_TOKEN_COOKIE_NAME", + Value: "GEN_REFRESH_TOKEN", + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteLaxMode, + Expires: time.Now().UTC().Add(time.Hour * 24 * 7), + } + + var data = response{ + IDToken: "GEN_ID_TOKEN", + AccessToken: "GEN_ACCESS_TOKEN", + } + + s.respondCookie(w, r, data, cookie) } } -type User struct { - Email internal.Email `json:"email"` - Username string `json:"username"` - Password internal.Password `json:"password"` +func (s *Service) handleLogout() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cookie := &http.Cookie{ + Path: "REFRESH_TOKEN_COOKIE_PATH", + Name: "REFRESH_TOKEN_COOKIE_NAME", + Value: "", + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteLaxMode, + Expires: time.Unix(0, 0), + } + + s.respondCookie(w, r, nil, cookie) + } } -func (u User) Decode(iu *internal.User) error { - h, err := u.Password.Hash() - if err != nil { - return err +func (s *Service) handleRefresh() http.HandlerFunc { + type response struct { + AccessToken string } - *iu = internal.User{ - ID: suid.NewUUID(), - Email: u.Email, - Username: u.Username, - Password: h, + return func(w http.ResponseWriter, r *http.Request) { + c, err := r.Cookie("REFRESH_TOKEN_COOKIE_NAME") + if err != nil { + s.respond(w, r, err, http.StatusUnauthorized) + return + } + + /* + Claims & Tokens + + Token generated by cookie value + */ + + *c = http.Cookie{ + Path: "REFRESH_TOKEN_COOKIE_PATH", + Name: "REFRESH_TOKEN_COOKIE_NAME", + Value: "GEN_REFRESH_TOKEN", + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteLaxMode, + Expires: time.Now().UTC().Add(time.Hour * 24 * 7), + } + + data := response{ + AccessToken: "GEN_ACCESS_TOKEN", + } + + s.respondCookie(w, r, data, c) } +} - return nil +func (s *Service) authenticate() func(f http.HandlerFunc) http.HandlerFunc { + return func(f http.HandlerFunc) http.HandlerFunc { + const ( + Auth = "Authorization" + ) + + return func(w http.ResponseWriter, r *http.Request) { + bearer := strings.Split(r.Header.Get(Auth), " ") + + if len(bearer) <= 1 { + s.respond(w, r, errors.New("invalid session"), http.StatusUnauthorized) + return + } + + // context with value + f(w, r) + } + } } + +// NOTE: internal/auth diff --git a/service/user/service.go b/service/user/service.go index 2d29e33f..4e9c5da4 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -14,7 +14,7 @@ import ( "github.com/rog-golang-buddies/rmx/test/mock" ) -// type contextKey string +type contextKey string // func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } @@ -50,10 +50,20 @@ func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, stat h.Respond(w, r, data, status) } +func (s *Service) respondCookie(w http.ResponseWriter, r *http.Request, data any, c *http.Cookie) { + http.SetCookie(w, c) + s.respond(w, r, data, http.StatusOK) +} + func (s *Service) created(w http.ResponseWriter, r *http.Request, id string) { h.Created(w, r, id) } +func (s *Service) createdCookie(w http.ResponseWriter, r *http.Request, id string, c *http.Cookie) { + http.SetCookie(w, c) + h.Created(w, r, id) +} + func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { return h.Decode(w, r, data) } @@ -64,12 +74,20 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, func (s *Service) routes() { s.m.Route("/api/v1/user", func(r chi.Router) { - r.Get("/", s.handleUserLogin()) - r.Post("/", s.handleUserSignUp()) // health r.Get("/ping", s.handlePing) }) + + s.m.Route("/api/v1/auth", func(r chi.Router) { + // r.Use(authService.CheckAuth) + + r.Post("/register", s.handleRegistration()) + r.Get("/refresh", s.handleRefresh()) + + r.Post("/login", s.handleLogin()) + r.Get("/logout", s.handleLogout()) + }) } func (s *Service) handlePing(w http.ResponseWriter, r *http.Request) { From c036ae19085fdd861bd1d038ecc19ea8f75f8693 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 1 Oct 2022 09:10:19 +0100 Subject: [PATCH 095/246] start on auth package --- internal/auth/auth.go | 1 + service/user/routes.go | 12 ++++-------- 2 files changed, 5 insertions(+), 8 deletions(-) create mode 100644 internal/auth/auth.go diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 00000000..8832b06d --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1 @@ +package auth diff --git a/service/user/routes.go b/service/user/routes.go index c8011f7d..3d239b83 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -72,8 +72,8 @@ func (s *Service) handleLogin() http.HandlerFunc { } type response struct { - IDToken string `json:"id_token"` - AccessToken string `json:"access_token"` + IDToken string `json:"idToken"` + AccessToken string `json:"accessToken"` } return func(w http.ResponseWriter, r *http.Request) { @@ -137,7 +137,7 @@ func (s *Service) handleLogout() http.HandlerFunc { func (s *Service) handleRefresh() http.HandlerFunc { type response struct { - AccessToken string + AccessToken string `json:"accessToken"` } return func(w http.ResponseWriter, r *http.Request) { @@ -173,12 +173,8 @@ func (s *Service) handleRefresh() http.HandlerFunc { func (s *Service) authenticate() func(f http.HandlerFunc) http.HandlerFunc { return func(f http.HandlerFunc) http.HandlerFunc { - const ( - Auth = "Authorization" - ) - return func(w http.ResponseWriter, r *http.Request) { - bearer := strings.Split(r.Header.Get(Auth), " ") + bearer := strings.Split(r.Header.Get("Authorization"), " ") if len(bearer) <= 1 { s.respond(w, r, errors.New("invalid session"), http.StatusUnauthorized) From c660f6031536ea08ca353585be68d6a1cdea3e19 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 1 Oct 2022 17:23:57 +0100 Subject: [PATCH 096/246] middleware using chi.With --- .gitignore | 1 + Makefile | 5 +++ cmd/cli.go | 4 +- cmd/main.go | 13 +++--- go.mod | 11 +++++ go.sum | 32 ++++++++++++++ internal/auth/auth.go | 1 - internal/auth/jwt_test.go | 81 ++++++++++++++++++++++++++++++++++++ internal/websocket/client.go | 2 + load_dotenv.sh | 5 --- service/jam/routes.go | 67 +++++++++++++++++++++++++++++ service/jam/service.go | 13 +++--- service/service.go | 3 +- service/user/service.go | 5 --- 14 files changed, 217 insertions(+), 26 deletions(-) delete mode 100644 internal/auth/auth.go create mode 100644 internal/auth/jwt_test.go delete mode 100644 load_dotenv.sh diff --git a/.gitignore b/.gitignore index d7fd8152..1fb33db8 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ bin/ .DS_Store *.env cmd/**/config +certs # NPM node_modules diff --git a/Makefile b/Makefile index f003933d..d0a3a137 100644 --- a/Makefile +++ b/Makefile @@ -40,3 +40,8 @@ fmt: .PHONY: build build: $(GOFLAGS) $(GO_BUILD) -a -v -ldflags="-w -s" -o bin/rmx-server cmd/*.go + +# --host should be from ENV +.PHONT: tls +tls: + go run /usr/local/go/src/crypto/tls/generate_cert.go --host=$(HOSTNAME) \ No newline at end of file diff --git a/cmd/cli.go b/cmd/cli.go index 1a8ebe53..503b1416 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -59,7 +59,9 @@ func initCLI() *cli.App { Port: port, } - return run(cfg) + _ = cfg + + return run() }, Flags: flags, }, diff --git a/cmd/main.go b/cmd/main.go index 514259ea..7125409c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,15 +5,14 @@ import ( "log" "net" "net/http" - "os" "os/signal" - "strconv" "syscall" "time" - "github.com/go-chi/chi/v5" "github.com/rs/cors" "golang.org/x/sync/errgroup" + + "github.com/rog-golang-buddies/rmx/service" ) // var ( @@ -21,12 +20,12 @@ import ( // ) func main() { - if err := initCLI().Run(os.Args); err != nil { + if err := run(); err != nil { log.Fatalln(err) } } -func run(cfg *Config) error { +func run() error { sCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) defer cancel() @@ -39,8 +38,8 @@ func run(cfg *Config) error { } srv := http.Server{ - Addr: ":" + strconv.Itoa(cfg.Port), - Handler: cors.New(c).Handler(chi.NewMux()), + Addr: ":8080", + Handler: cors.New(c).Handler(service.Default()), // max time to read request from the client ReadTimeout: 10 * time.Second, // max time to write response to the client diff --git a/go.mod b/go.mod index c569e248..238ec38d 100644 --- a/go.mod +++ b/go.mod @@ -20,10 +20,21 @@ require ( golang.org/x/term v0.0.0-20220919170432-7a66f970e087 ) +require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/goccy/go-json v0.9.11 // indirect + github.com/lestrrat-go/blackmagic v1.0.1 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.4 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.0 // indirect +) + require ( github.com/BurntSushi/toml v1.1.0 // indirect github.com/containerd/console v1.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/lestrrat-go/jwx/v2 v2.0.6 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-localereader v0.0.1 // indirect diff --git a/go.sum b/go.sum index 0ce91064..38bb35d9 100644 --- a/go.sum +++ b/go.sum @@ -14,10 +14,17 @@ github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARu github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -25,6 +32,18 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/hyphengolang/prelude v0.0.7 h1:AgLkbASkg/3ioIjr0JinhReTiaeppIaX2sa7vrFwaoc= github.com/hyphengolang/prelude v0.0.7/go.mod h1:obKnO4SQhPfu3gqA9g29nbDRQLNTtmJy02i7KYZGdfM= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= +github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= +github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.0.6 h1:RlyYNLV892Ed7+FTfj1ROoF6x7WxL965PGTHso/60G0= +github.com/lestrrat-go/jwx/v2 v2.0.6/go.mod h1:aVrGuwEr3cp2Prw6TtQvr8sQxe+84gruID5C9TxT64Q= +github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -49,6 +68,7 @@ github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKt github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -57,32 +77,44 @@ github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk= github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20220921164117-439092de6870 h1:j8b6j9gzSigH28O5SjSpQSSh9lFd6f5D/q0aHjNTulc= golang.org/x/exp v0.0.0-20220921164117-439092de6870/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w= golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/auth/auth.go b/internal/auth/auth.go deleted file mode 100644 index 8832b06d..00000000 --- a/internal/auth/auth.go +++ /dev/null @@ -1 +0,0 @@ -package auth diff --git a/internal/auth/jwt_test.go b/internal/auth/jwt_test.go new file mode 100644 index 00000000..1902b419 --- /dev/null +++ b/internal/auth/jwt_test.go @@ -0,0 +1,81 @@ +package auth + +import ( + "fmt" + "net/http" + "testing" + + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" +) + +var rsaPrivateKey = `-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAML5MHFgqUlZcENS +hHZ83yXfoUpqaMfp5/UdgMIJ0S5DW5QEON6reAsDu6zP0BEVZhg65pEYWEraBrGK +Vcbx7dsVqK4Z0GMm0YRAvB+1K+pYlXwld90mwG1TqOKDPQXqC0Z/jZi6DSsAhfJU +WN0rkInZRtoVeRzbbh+nLN8nd14fAgMBAAECgYEAor+A2VL3XBvFIt0RZxpq5mFa +cBSMrDsqfSeIX+/z5SsimVZA5lW5GXCfSuwY4Pm8xAL+jSUGJk0CA1bWrP8rLByS +cQAy1q0odaAiWIG5zFUEQBg5Q5b3+jXmh2zwtO7yhPuXn1/vBGg+FvyR57gV/3F+ +TuBfR6Bc3VWKuj7Gm5kCQQDuRgm8HTDbX7IQ0EFAVKB73Pj4Gx5u2NieD9U8+qXx +JsAdn1vRvQ3mNJDR5OcTr4rPkpLLCtzjA2iTDXp4yqmrAkEA0Xp91LCpImKAOtM3 +4SGXdzKi9+7fWmcTtfkz996y9A1C9l27Cj92P7OFdwMB4Z/ZMizJd0eXYhXr4IxH +wBoxXQJAUBOXp/HDfqZdiIsEsuL+AEKWJYOvqZ8UxaIajuDJrg7Q1+O7jvRTXH9k +ADZGdnYzV2kyDiy7aUu29Fy+QSQS+wJAJyEsdBhz35pqvZJK8+DkfD2XN50FV8u9 +YNamIH0XDIOVqJOlpqpoGkocejizl0PWvIqlL4TOAGJ75zwNAxNheQJABEA2/hfF +GMJsOrnD74rGP/Lfpg882AmeUoT5eH766sSobFfUYJZvyAmnQoK2Lzg2hrKwXXix +JvEGfrhihVLb7g== +-----END PRIVATE KEY----- +` + +func TestJWT(t *testing.T) { + // -- Parse, Serialize JSON Web Key -- + jwkPrivate, err := jwk.ParseKey([]byte(rsaPrivateKey), jwk.WithPEM(true)) + if err != nil { + t.Fatalf("failed to parse JWK: %s\n", err) + } + + jwtPublic, err := jwk.PublicKeyOf(jwkPrivate) + if err != nil { + t.Fatalf("failed to get public key: %s\n", err) + } + // -- Parse, Serialize -- + + // -- Working with JSON Web Tokens -- + // build (server boot?) + token, err := jwt.NewBuilder().Build() + if err != nil { + t.Fatalf("failed to get public key: %s\n", err) + } + + // sign (server boot?) + signed, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, jwkPrivate)) + if err != nil { + t.Fatalf("failed to sign token: %s\n", err) + return + } + + // verify (every endpoint hit) + // verified, err := jwt.Parse(signed, jwt.WithKey(jwa.RS256, jwtPublic)) + // if err != nil { + // fmt.Printf("failed to verify JWS: %s\n", err) + // return + // } + // _ = verified + + req, err := http.NewRequest(http.MethodGet, `http://localhost:8080`, nil) + req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, signed)) + if err != nil { + t.Fatalf("failed to create HTTP request: %s\n", err) + return + } + + verifiedToken, err := jwt.ParseRequest(req, jwt.WithKey(jwa.RS256, jwtPublic)) + if err != nil { + t.Fatalf("failed to verify token from HTTP request: %s\n", err) + return + } + + _ = verifiedToken + // -- Working with htt.Request -- +} diff --git a/internal/websocket/client.go b/internal/websocket/client.go index 2b89359d..a7126272 100644 --- a/internal/websocket/client.go +++ b/internal/websocket/client.go @@ -1,6 +1,7 @@ package websocket import ( + "log" "sync" rmx "github.com/rog-golang-buddies/rmx/internal" @@ -48,6 +49,7 @@ func (c *Client) Get(uid suid.UUID) (*Pool, error) { defer c.mu.Unlock() for id, p := range c.ps { + log.Println("is match?", id, uid, id == uid) if id == uid { return p, nil } diff --git a/load_dotenv.sh b/load_dotenv.sh deleted file mode 100644 index 27e2be25..00000000 --- a/load_dotenv.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -if [ ! -f .env ]; then - export "cmd .env | .." -fi diff --git a/service/jam/routes.go b/service/jam/routes.go index 99ad9874..b99cffbd 100644 --- a/service/jam/routes.go +++ b/service/jam/routes.go @@ -21,6 +21,8 @@ func (s *Service) handleCreateRoom() http.HandlerFunc { return } + s.l.Println("create a room", s.c.Size()) + v := &session{ID: suid.FromUUID(uid)} s.respond(w, r, v, http.StatusOK) @@ -72,6 +74,70 @@ func (s Service) handleListRooms() http.HandlerFunc { } } +// Works with `chi.With` +func (s *Service) _connectionPool(p *ws.Pool) func(f http.Handler) http.Handler { + return func(f http.Handler) http.Handler { + var fn func(w http.ResponseWriter, r *http.Request) + if p != nil { + fn = func(w http.ResponseWriter, r *http.Request) { + f.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), roomKey, p))) + } + } else { + fn = func(w http.ResponseWriter, r *http.Request) { + s.l.Println(r.URL.Path) + + uid, err := s.parseUUID(w, r) + if err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + p, err := s.c.Get(uid) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + r = r.WithContext(context.WithValue(r.Context(), roomKey, p)) + f.ServeHTTP(w, r) + } + } + + return http.HandlerFunc(fn) + } +} + +func (s Service) _upgradeHTTP(readBuf, writeBuf int) func(f http.Handler) http.Handler { + return func(f http.Handler) http.Handler { + u := &websocket.Upgrader{ + ReadBufferSize: readBuf, + WriteBufferSize: writeBuf, + CheckOrigin: func(r *http.Request) bool { return true }, + } + + fn := func(w http.ResponseWriter, r *http.Request) { + p, _ := r.Context().Value(roomKey).(*ws.Pool) + if p.Size() == p.MaxConn { + s.respond(w, r, ws.ErrMaxConn, http.StatusUnauthorized) + return + } + + c, err := p.NewConn(w, r, u) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + r = r.WithContext(context.WithValue(r.Context(), upgradeKey, c)) + f.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) + } +} + +// converting to handler for middleware, in order to use Chi's default type + func (s *Service) connectionPool(p *ws.Pool) func(f http.HandlerFunc) http.HandlerFunc { return func(f http.HandlerFunc) http.HandlerFunc { if p != nil { @@ -148,6 +214,7 @@ func (s Service) handleP2PComms() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { c := r.Context().Value(upgradeKey).(*ws.Conn) + defer func() { // FIXME send error when Leaving session pool c.SendMessage(response[leave]{ diff --git a/service/jam/service.go b/service/jam/service.go index 37315493..1f129215 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -5,7 +5,6 @@ import ( "log" "net/http" - "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" h "github.com/hyphengolang/prelude/http" @@ -79,23 +78,25 @@ func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{ } func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { + ctx := chi.RouteContext(r.Context()) + s.l.Println(ctx.URLParams.Values, len(ctx.URLParams.Values)) return suid.ParseString(chi.URLParam(r, "uuid")) } func (s *Service) routes() { - s.m.Use(middleware.Logger) - s.m.Route("/api/v1/jam", func(r chi.Router) { r.Get("/", s.handleListRooms()) r.Post("/", s.handleCreateRoom()) r.Get("/{uuid}", s.handleGetRoom()) - // health r.Get("/ping", s.handlePing) }) - // Websocket - s.m.Get("/ws/jam/{id}", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.connectionPool(nil))) + // s.m.Get("/ws/jam/{uuid}", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.connectionPool(nil))) + s.m.Route("/ws/jam", func(r chi.Router) { + r = r.With(s._connectionPool(nil), s._upgradeHTTP(1024, 1024)) + r.Get("/{uuid}", s.handleP2PComms()) + }) } func (s *Service) handlePing(w http.ResponseWriter, r *http.Request) { diff --git a/service/service.go b/service/service.go index 7dcd2b8b..a8707453 100644 --- a/service/service.go +++ b/service/service.go @@ -22,12 +22,13 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeH func New(m chi.Router, ur internal.UserRepo) *Service { s := &Service{m, log.Default()} - s.m.Use(middleware.Logger) // NOTE unsure how much is gained using a goroutine // will have to investigate go jam.NewService(s.m) go user.NewService(s.m, ur) + + s.m.Use(middleware.Logger) return s } diff --git a/service/user/service.go b/service/user/service.go index 4e9c5da4..ed080226 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -59,11 +59,6 @@ func (s *Service) created(w http.ResponseWriter, r *http.Request, id string) { h.Created(w, r, id) } -func (s *Service) createdCookie(w http.ResponseWriter, r *http.Request, id string, c *http.Cookie) { - http.SetCookie(w, c) - h.Created(w, r, id) -} - func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { return h.Decode(w, r, data) } From 7bd42b4499221a0f64ad4e4e72df6b51efced2a5 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 1 Oct 2022 17:48:40 +0100 Subject: [PATCH 097/246] remove redundant handlers --- service/jam/routes.go | 98 ++---------------------------------------- service/jam/service.go | 2 +- 2 files changed, 5 insertions(+), 95 deletions(-) diff --git a/service/jam/routes.go b/service/jam/routes.go index b99cffbd..024e2154 100644 --- a/service/jam/routes.go +++ b/service/jam/routes.go @@ -54,7 +54,7 @@ func (s *Service) handleGetRoom() http.HandlerFunc { } } -func (s Service) handleListRooms() http.HandlerFunc { +func (s *Service) handleListRooms() http.HandlerFunc { type response struct { Sessions []session `json:"sessions"` } @@ -75,7 +75,7 @@ func (s Service) handleListRooms() http.HandlerFunc { } // Works with `chi.With` -func (s *Service) _connectionPool(p *ws.Pool) func(f http.Handler) http.Handler { +func (s *Service) connectionPool(p *ws.Pool) func(f http.Handler) http.Handler { return func(f http.Handler) http.Handler { var fn func(w http.ResponseWriter, r *http.Request) if p != nil { @@ -107,7 +107,7 @@ func (s *Service) _connectionPool(p *ws.Pool) func(f http.Handler) http.Handler } } -func (s Service) _upgradeHTTP(readBuf, writeBuf int) func(f http.Handler) http.Handler { +func (s *Service) upgradeHTTP(readBuf, writeBuf int) func(f http.Handler) http.Handler { return func(f http.Handler) http.Handler { u := &websocket.Upgrader{ ReadBufferSize: readBuf, @@ -138,61 +138,7 @@ func (s Service) _upgradeHTTP(readBuf, writeBuf int) func(f http.Handler) http.H // converting to handler for middleware, in order to use Chi's default type -func (s *Service) connectionPool(p *ws.Pool) func(f http.HandlerFunc) http.HandlerFunc { - return func(f http.HandlerFunc) http.HandlerFunc { - if p != nil { - return func(w http.ResponseWriter, r *http.Request) { - f(w, r.WithContext(context.WithValue(r.Context(), roomKey, p))) - } - } - - return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r) - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - p, err := s.c.Get(uid) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - r = r.WithContext(context.WithValue(r.Context(), roomKey, p)) - f(w, r) - } - } -} - -func (s Service) upgradeHTTP(readBuf, writeBuf int) func(f http.HandlerFunc) http.HandlerFunc { - return func(f http.HandlerFunc) http.HandlerFunc { - u := &websocket.Upgrader{ - ReadBufferSize: readBuf, - WriteBufferSize: writeBuf, - CheckOrigin: func(r *http.Request) bool { return true }, - } - - return func(w http.ResponseWriter, r *http.Request) { - p := r.Context().Value(roomKey).(*ws.Pool) - if p.Size() == p.MaxConn { - s.respond(w, r, ws.ErrMaxConn, http.StatusUnauthorized) - return - } - - c, err := p.NewConn(w, r, u) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - r = r.WithContext(context.WithValue(r.Context(), upgradeKey, c)) - f(w, r) - } - } -} - -func (s Service) handleP2PComms() http.HandlerFunc { +func (s *Service) handleP2PComms() http.HandlerFunc { // FIXME we will change this as I know this hasn't been // was just my way of getting things working, not yet // full agreement with this. @@ -252,39 +198,3 @@ func (s Service) handleP2PComms() http.HandlerFunc { } } } - -/* TODO only here due to testing but will leave out for now -func (s Service) handleEcho() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - c := r.Context().Value(upgradeKey).(*ws.Conn) - defer func() { - // ! send error when Leaving session pool - c.SendMessage("leave") - - s.l.Println("default leave") - - c.Close() - }() - - if err := c.SendMessage("join"); err != nil { - s.l.Println(err) - return - } - - s.l.Println("default join") - - for { - var msg any - if err := c.ReadJSON(&msg); err != nil { - s.l.Println(err) - return - } - - if err := c.SendMessage(msg); err != nil { - s.l.Println(err) - return - } - } - } -} -*/ diff --git a/service/jam/service.go b/service/jam/service.go index 1f129215..7ccf0cd3 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -94,7 +94,7 @@ func (s *Service) routes() { // s.m.Get("/ws/jam/{uuid}", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.connectionPool(nil))) s.m.Route("/ws/jam", func(r chi.Router) { - r = r.With(s._connectionPool(nil), s._upgradeHTTP(1024, 1024)) + r = r.With(s.connectionPool(nil), s.upgradeHTTP(1024, 1024)) r.Get("/{uuid}", s.handleP2PComms()) }) } From 1c4e1823e6672dd15647ab4af20ffb2d771befba Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 1 Oct 2022 17:59:32 +0100 Subject: [PATCH 098/246] reset cli --- cmd/cli.go | 4 +--- cmd/main.go | 8 +++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/cli.go b/cmd/cli.go index 503b1416..1a8ebe53 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -59,9 +59,7 @@ func initCLI() *cli.App { Port: port, } - _ = cfg - - return run() + return run(cfg) }, Flags: flags, }, diff --git a/cmd/main.go b/cmd/main.go index 7125409c..7da903bd 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,7 +5,9 @@ import ( "log" "net" "net/http" + "os" "os/signal" + "strconv" "syscall" "time" @@ -20,12 +22,12 @@ import ( // ) func main() { - if err := run(); err != nil { + if err := initCLI().Run(os.Args); err != nil { log.Fatalln(err) } } -func run() error { +func run(cfg *Config) error { sCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) defer cancel() @@ -38,7 +40,7 @@ func run() error { } srv := http.Server{ - Addr: ":8080", + Addr: ":" + strconv.Itoa(cfg.Port), Handler: cors.New(c).Handler(service.Default()), // max time to read request from the client ReadTimeout: 10 * time.Second, From 214974dbfe35e42c3e89c88636c1adedb4d11480 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 1 Oct 2022 18:23:13 +0100 Subject: [PATCH 099/246] handler_register, handle_login, fleshing out --- service/jam/service.go | 2 -- service/user/routes.go | 17 ++++++++++------- service/user/routes_test.go | 17 ++++++++++++++++- service/user/service.go | 20 +++++++++++++------- 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/service/jam/service.go b/service/jam/service.go index 7ccf0cd3..e8a81254 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -78,8 +78,6 @@ func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{ } func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { - ctx := chi.RouteContext(r.Context()) - s.l.Println(ctx.URLParams.Values, len(ctx.URLParams.Values)) return suid.ParseString(chi.URLParam(r, "uuid")) } diff --git a/service/user/routes.go b/service/user/routes.go index 3d239b83..afc72e63 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -122,9 +122,9 @@ func (s *Service) handleLogin() http.HandlerFunc { func (s *Service) handleLogout() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cookie := &http.Cookie{ - Path: "REFRESH_TOKEN_COOKIE_PATH", - Name: "REFRESH_TOKEN_COOKIE_NAME", - Value: "", + Path: "REFRESH_TOKEN_COOKIE_PATH", + Name: "REFRESH_TOKEN_COOKIE_NAME", + // Value: "", HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteLaxMode, @@ -171,9 +171,10 @@ func (s *Service) handleRefresh() http.HandlerFunc { } } -func (s *Service) authenticate() func(f http.HandlerFunc) http.HandlerFunc { - return func(f http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +// get the Bearer +func (s *Service) authenticate() func(f http.Handler) http.Handler { + return func(f http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { bearer := strings.Split(r.Header.Get("Authorization"), " ") if len(bearer) <= 1 { @@ -182,8 +183,10 @@ func (s *Service) authenticate() func(f http.HandlerFunc) http.HandlerFunc { } // context with value - f(w, r) + f.ServeHTTP(w, r) } + + return http.HandlerFunc(fn) } } diff --git a/service/user/routes_test.go b/service/user/routes_test.go index 5af6d41b..a7b14a0e 100644 --- a/service/user/routes_test.go +++ b/service/user/routes_test.go @@ -13,7 +13,7 @@ func TestRoutes(t *testing.T) { s := httptest.NewServer(srv) t.Cleanup(func() { s.Close() }) - // TODO|FIXME needs to return an error if + // TODO add tests when - // `password`, `email` or `username` is not present payload := ` { @@ -30,4 +30,19 @@ func TestRoutes(t *testing.T) { if r.StatusCode != http.StatusCreated { t.Fatalf("expected %d; got %d", http.StatusCreated, r.StatusCode) } + + payload = ` +{ + "email":"user@gmail.com", + "password":"difficultPassword" +}` + + r, err = s.Client().Post(s.URL+"/api/v1/auth/login", "application/json", strings.NewReader(payload)) + if err != nil { + t.Fatal(err) + } + + if r.StatusCode != http.StatusOK { + t.Fatalf("expected %d; got %d", http.StatusCreated, r.StatusCode) + } } diff --git a/service/user/service.go b/service/user/service.go index ed080226..425a3b23 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -64,24 +64,30 @@ func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{ } func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { - return suid.ParseString(chi.URLParam(r, "id")) + return suid.ParseString(chi.URLParam(r, "uuid")) } func (s *Service) routes() { s.m.Route("/api/v1/user", func(r chi.Router) { + auth := r.With(s.authenticate()) + auth.Get("/me", func(w http.ResponseWriter, r *http.Request) { + // authorization required + s.respond(w, r, nil, http.StatusNotImplemented) + }) + + r.Post("/", s.handleRegistration()) // health r.Get("/ping", s.handlePing) + }) s.m.Route("/api/v1/auth", func(r chi.Router) { - // r.Use(authService.CheckAuth) - - r.Post("/register", s.handleRegistration()) - r.Get("/refresh", s.handleRefresh()) - r.Post("/login", s.handleLogin()) - r.Get("/logout", s.handleLogout()) + + auth := r.With(s.authenticate()) + auth.Get("/refresh", s.handleRefresh()) + auth.Post("/logout", s.handleLogout()) }) } From 9030db0aa021201d6a81adf815b8007fd3041d5a Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 1 Oct 2022 22:09:00 +0100 Subject: [PATCH 100/246] auth works? --- service/user/routes.go | 98 ++++++++++++++++++++++++++++--------- service/user/routes_test.go | 48 +++++++++++++++++- service/user/service.go | 45 +++++++++++++---- 3 files changed, 157 insertions(+), 34 deletions(-) diff --git a/service/user/routes.go b/service/user/routes.go index afc72e63..51995168 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -1,11 +1,13 @@ package user import ( - "errors" + "context" "net/http" - "strings" "time" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/suid" ) @@ -20,6 +22,17 @@ import ( // 412 - Invalid precondition // 422 - Unprocessable +func (s *Service) handleMyInfo() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + payload := ` +{ + "name":"foobar" +}` + + s.respond(w, r, payload, http.StatusOK) + } +} + type signup struct { Email internal.Email `json:"email"` Username string `json:"username"` @@ -65,7 +78,7 @@ func (s *Service) handleRegistration() http.HandlerFunc { } } -func (s *Service) handleLogin() http.HandlerFunc { +func (s *Service) handleLogin(privateKey, publicKey jwk.Key) http.HandlerFunc { type login struct { Email internal.Email `json:"email"` Password internal.Password `json:"password"` @@ -94,16 +107,52 @@ func (s *Service) handleLogin() http.HandlerFunc { return } - /* - Claims & Token + // -- Generate Tokens -- + var now = time.Now().UTC() + var jwb = jwt.NewBuilder().Issuer("github.com/rog-golang-buddies/rmx").IssuedAt(now).Claim("email", u.Email) + // Audience([]string{"http://localhost:3000"}). + _ = jwb - Token generated by the user - */ + idToken, err := jwb.Subject(string(u.Email)).Expiration(now.Add(time.Hour * 10)).Build() + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + idTokenSigned, err := jwt.Sign(idToken, jwt.WithKey(jwa.RS256, privateKey)) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + accessToken, err := jwt.NewBuilder().Subject(u.ID.String()).Expiration(now.Add(time.Minute * 5)).Build() + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + accessTokenSigned, err := jwt.Sign(accessToken, jwt.WithKey(jwa.RS256, privateKey)) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + refreshToken, err := jwt.NewBuilder().Subject(u.ID.String()).Expiration(now.Add(time.Hour * 24 * 7)).Build() + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + refreshTokenSigned, err := jwt.Sign(refreshToken, jwt.WithKey(jwa.RS256, privateKey)) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + // -- Generate Tokens -- cookie := &http.Cookie{ - Path: "REFRESH_TOKEN_COOKIE_PATH", - Name: "REFRESH_TOKEN_COOKIE_NAME", - Value: "GEN_REFRESH_TOKEN", + Name: "RMX_DIRECT_RT", + Value: string(refreshTokenSigned), HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteLaxMode, @@ -111,8 +160,8 @@ func (s *Service) handleLogin() http.HandlerFunc { } var data = response{ - IDToken: "GEN_ID_TOKEN", - AccessToken: "GEN_ACCESS_TOKEN", + IDToken: string(idTokenSigned), + AccessToken: string(accessTokenSigned), } s.respondCookie(w, r, data, cookie) @@ -122,9 +171,9 @@ func (s *Service) handleLogin() http.HandlerFunc { func (s *Service) handleLogout() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cookie := &http.Cookie{ - Path: "REFRESH_TOKEN_COOKIE_PATH", - Name: "REFRESH_TOKEN_COOKIE_NAME", - // Value: "", + // Path: "REFRESH_TOKEN_COOKIE_PATH", + Name: "RMX_DIRECT_RT", + Value: "", HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteLaxMode, @@ -171,23 +220,26 @@ func (s *Service) handleRefresh() http.HandlerFunc { } } -// get the Bearer -func (s *Service) authenticate() func(f http.Handler) http.Handler { +func (s *Service) authenticate(privateKey, publicKey jwk.Key) func(f http.Handler) http.Handler { return func(f http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - bearer := strings.Split(r.Header.Get("Authorization"), " ") + token, err := jwt.ParseRequest(r, jwt.WithKey(jwa.RS256, publicKey), jwt.WithValidate(true)) + if err != nil { + s.respond(w, r, err, http.StatusUnauthorized) + return + } - if len(bearer) <= 1 { - s.respond(w, r, errors.New("invalid session"), http.StatusUnauthorized) + claims := token.PrivateClaims() + email, ok := claims["email"].(string) + if !ok { + s.respond(w, r, "unauthorized", http.StatusUnauthorized) return } - // context with value + r = r.WithContext(context.WithValue(r.Context(), emailKey, email)) f.ServeHTTP(w, r) } return http.HandlerFunc(fn) } } - -// NOTE: internal/auth diff --git a/service/user/routes_test.go b/service/user/routes_test.go index a7b14a0e..2e454cfb 100644 --- a/service/user/routes_test.go +++ b/service/user/routes_test.go @@ -1,13 +1,15 @@ package user import ( + "encoding/json" + "fmt" "net/http" "net/http/httptest" "strings" "testing" ) -func TestRoutes(t *testing.T) { +func TestLogin(t *testing.T) { srv := DefaultService() s := httptest.NewServer(srv) @@ -41,8 +43,50 @@ func TestRoutes(t *testing.T) { if err != nil { t.Fatal(err) } + defer r.Body.Close() + + type response struct { + IDToken string `json:"idToken"` + AccessToken string `json:"accessToken"` + // PublicKey string `json:"publicKey"` + } + + var tokens response + if err := json.NewDecoder(r.Body).Decode(&tokens); err != nil { + t.Fatal(err) + } if r.StatusCode != http.StatusOK { - t.Fatalf("expected %d; got %d", http.StatusCreated, r.StatusCode) + t.Fatalf("expected %d; got %d", http.StatusOK, r.StatusCode) + } + + // get my user info + req, _ := http.NewRequest(http.MethodGet, s.URL+"/api/v1/user/me", nil) + req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, tokens.IDToken)) + + r, err = s.Client().Do(req) + if err != nil { + t.Fatal(err) + } + + if r.StatusCode != http.StatusOK { + t.Fatalf("expected %d; got %d", http.StatusOK, r.StatusCode) } } + +func TestAuthMe(t *testing.T) { + srv := DefaultService() + + s := httptest.NewServer(srv) + t.Cleanup(func() { s.Close() }) + + r, err := s.Client().Get(s.URL + "/api/v1/user/me") + if err != nil { + t.Fatal(err) + } + + if r.StatusCode != http.StatusOK { + t.Fatalf("expected %d; got %d", http.StatusOK, r.StatusCode) + } + +} diff --git a/service/user/service.go b/service/user/service.go index 425a3b23..dcd570de 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/go-chi/chi/v5" + "github.com/lestrrat-go/jwx/v2/jwk" h "github.com/hyphengolang/prelude/http" @@ -14,9 +15,27 @@ import ( "github.com/rog-golang-buddies/rmx/test/mock" ) +var secretTest = `-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAML5MHFgqUlZcENS +hHZ83yXfoUpqaMfp5/UdgMIJ0S5DW5QEON6reAsDu6zP0BEVZhg65pEYWEraBrGK +Vcbx7dsVqK4Z0GMm0YRAvB+1K+pYlXwld90mwG1TqOKDPQXqC0Z/jZi6DSsAhfJU +WN0rkInZRtoVeRzbbh+nLN8nd14fAgMBAAECgYEAor+A2VL3XBvFIt0RZxpq5mFa +cBSMrDsqfSeIX+/z5SsimVZA5lW5GXCfSuwY4Pm8xAL+jSUGJk0CA1bWrP8rLByS +cQAy1q0odaAiWIG5zFUEQBg5Q5b3+jXmh2zwtO7yhPuXn1/vBGg+FvyR57gV/3F+ +TuBfR6Bc3VWKuj7Gm5kCQQDuRgm8HTDbX7IQ0EFAVKB73Pj4Gx5u2NieD9U8+qXx +JsAdn1vRvQ3mNJDR5OcTr4rPkpLLCtzjA2iTDXp4yqmrAkEA0Xp91LCpImKAOtM3 +4SGXdzKi9+7fWmcTtfkz996y9A1C9l27Cj92P7OFdwMB4Z/ZMizJd0eXYhXr4IxH +wBoxXQJAUBOXp/HDfqZdiIsEsuL+AEKWJYOvqZ8UxaIajuDJrg7Q1+O7jvRTXH9k +ADZGdnYzV2kyDiy7aUu29Fy+QSQS+wJAJyEsdBhz35pqvZJK8+DkfD2XN50FV8u9 +YNamIH0XDIOVqJOlpqpoGkocejizl0PWvIqlL4TOAGJ75zwNAxNheQJABEA2/hfF +GMJsOrnD74rGP/Lfpg882AmeUoT5eH766sSobFfUYJZvyAmnQoK2Lzg2hrKwXXix +JvEGfrhihVLb7g== +-----END PRIVATE KEY----- +` + type contextKey string -// func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } +var emailKey = contextKey("rmx-email") var ( ErrNoCookie = errors.New("user: cookie not found") @@ -68,24 +87,32 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, } func (s *Service) routes() { + // setup auth private/public key pair from key.pem + private, err := jwk.ParseKey([]byte(secretTest), jwk.WithPEM(true)) + if err != nil { + panic(err) + } + + public, err := jwk.PublicKeyOf(private) + if err != nil { + panic(err) + } + // setup auth here + s.m.Route("/api/v1/user", func(r chi.Router) { - auth := r.With(s.authenticate()) - auth.Get("/me", func(w http.ResponseWriter, r *http.Request) { - // authorization required - s.respond(w, r, nil, http.StatusNotImplemented) - }) + auth := r.With(s.authenticate(private, public)) + auth.Get("/me", s.handleMyInfo()) r.Post("/", s.handleRegistration()) // health r.Get("/ping", s.handlePing) - }) s.m.Route("/api/v1/auth", func(r chi.Router) { - r.Post("/login", s.handleLogin()) + r.Post("/login", s.handleLogin(private, public)) - auth := r.With(s.authenticate()) + auth := r.With(s.authenticate(private, public)) auth.Get("/refresh", s.handleRefresh()) auth.Post("/logout", s.handleLogout()) }) From 528bf00c53b03c9a9f51f8a349fd9db5ec29aebb Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 1 Oct 2022 22:21:36 +0100 Subject: [PATCH 101/246] prep for cleanup --- service/user/routes.go | 28 ++++++++++++---------------- service/user/routes_test.go | 11 +++++++++-- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/service/user/routes.go b/service/user/routes.go index 51995168..51c6ba60 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -22,24 +22,13 @@ import ( // 412 - Invalid precondition // 422 - Unprocessable -func (s *Service) handleMyInfo() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - payload := ` -{ - "name":"foobar" -}` - - s.respond(w, r, payload, http.StatusOK) - } -} - -type signup struct { +type signupUser struct { Email internal.Email `json:"email"` Username string `json:"username"` Password internal.Password `json:"password"` } -func (v signup) decode(iu *internal.User) error { +func (v signupUser) decode(iu *internal.User) error { h, err := v.Password.Hash() if err != nil { return err @@ -57,7 +46,7 @@ func (v signup) decode(iu *internal.User) error { func (s *Service) handleRegistration() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var v signup + var v signupUser if err := s.decode(w, r, &v); err != nil { s.respond(w, r, err, http.StatusBadRequest) return @@ -78,6 +67,14 @@ func (s *Service) handleRegistration() http.HandlerFunc { } } +func (s *Service) handleMyInfo() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + email := r.Context().Value(emailKey).(string) + + s.respond(w, r, email, http.StatusOK) + } +} + func (s *Service) handleLogin(privateKey, publicKey jwk.Key) http.HandlerFunc { type login struct { Email internal.Email `json:"email"` @@ -229,8 +226,7 @@ func (s *Service) authenticate(privateKey, publicKey jwk.Key) func(f http.Handle return } - claims := token.PrivateClaims() - email, ok := claims["email"].(string) + email, ok := token.PrivateClaims()["email"].(string) if !ok { s.respond(w, r, "unauthorized", http.StatusUnauthorized) return diff --git a/service/user/routes_test.go b/service/user/routes_test.go index 2e454cfb..62638ba6 100644 --- a/service/user/routes_test.go +++ b/service/user/routes_test.go @@ -72,6 +72,13 @@ func TestLogin(t *testing.T) { if r.StatusCode != http.StatusOK { t.Fatalf("expected %d; got %d", http.StatusOK, r.StatusCode) } + + var str string + if err := json.NewDecoder(r.Body).Decode(&str); err != nil { + t.Fatal(err) + } + + t.Log(str) } func TestAuthMe(t *testing.T) { @@ -85,8 +92,8 @@ func TestAuthMe(t *testing.T) { t.Fatal(err) } - if r.StatusCode != http.StatusOK { - t.Fatalf("expected %d; got %d", http.StatusOK, r.StatusCode) + if r.StatusCode != http.StatusUnauthorized { + t.Fatalf("expected %d; got %d", http.StatusUnauthorized, r.StatusCode) } } From a7028297e208beda4c40b24072e326a50b267418 Mon Sep 17 00:00:00 2001 From: Parham Moieni Date: Sun, 2 Oct 2022 01:16:51 +0330 Subject: [PATCH 102/246] small fix good progress. but remember we'll need Redis for storing used refresh tokens. otherwise if a token is stolen we can't detect it and close the authentication flow. and to allow the user to have multiple devices connected we need a `clientId` field inside token claims. this will allow us to recognize which device a user is using so we can only close that specific connection and let others untouched. --- go.sum | 3 +++ service/user/routes.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go.sum b/go.sum index 38bb35d9..a2af93e1 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,7 @@ github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkX github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= @@ -68,6 +69,7 @@ github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKt github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= @@ -81,6 +83,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk= github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= diff --git a/service/user/routes.go b/service/user/routes.go index 51c6ba60..ad3cd49e 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -228,7 +228,7 @@ func (s *Service) authenticate(privateKey, publicKey jwk.Key) func(f http.Handle email, ok := token.PrivateClaims()["email"].(string) if !ok { - s.respond(w, r, "unauthorized", http.StatusUnauthorized) + s.respond(w, r, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } From 5d1c31544f4a69ca15b4fb80fa9eb64c78926103 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 1 Oct 2022 22:58:58 +0100 Subject: [PATCH 103/246] moved tokens to a helper --- service/user/routes.go | 104 ++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/service/user/routes.go b/service/user/routes.go index 51c6ba60..a751a878 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -67,7 +67,7 @@ func (s *Service) handleRegistration() http.HandlerFunc { } } -func (s *Service) handleMyInfo() http.HandlerFunc { +func (s *Service) handleIdentity() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { email := r.Context().Value(emailKey).(string) @@ -75,19 +75,62 @@ func (s *Service) handleMyInfo() http.HandlerFunc { } } -func (s *Service) handleLogin(privateKey, publicKey jwk.Key) http.HandlerFunc { - type login struct { +func (s *Service) signedTokens(key jwk.Key, now time.Time, email, uuid string) (its, ats, rts []byte, err error) { + // -- Generate Tokens -- + // var now = time.Now().UTC() + var jwb = jwt.NewBuilder().Issuer("github.com/rog-golang-buddies/rmx").IssuedAt(now).Claim("email", email) + // Audience([]string{"http://localhost:3000"}). + + it, err := jwb.Subject(email).Expiration(now.Add(time.Hour * 10)).Build() + if err != nil { + return nil, nil, nil, err + + } + + its, err = jwt.Sign(it, jwt.WithKey(jwa.RS256, key)) + if err != nil { + return nil, nil, nil, err + + } + + at, err := jwt.NewBuilder().Subject(uuid).Expiration(now.Add(time.Minute * 5)).Build() + if err != nil { + return nil, nil, nil, err + } + + ats, err = jwt.Sign(at, jwt.WithKey(jwa.RS256, key)) + if err != nil { + return nil, nil, nil, err + + } + + rt, err := jwt.NewBuilder().Subject(uuid).Expiration(now.Add(time.Hour * 24 * 7)).Build() + if err != nil { + return nil, nil, nil, err + + } + + rts, err = jwt.Sign(rt, jwt.WithKey(jwa.RS256, key)) + if err != nil { + return nil, nil, nil, err + } + + return its, ats, rts, nil +} + +func (s *Service) handleLogin(privateKey jwk.Key) http.HandlerFunc { + type loginUser struct { Email internal.Email `json:"email"` Password internal.Password `json:"password"` } - type response struct { + type authTokens struct { IDToken string `json:"idToken"` AccessToken string `json:"accessToken"` } return func(w http.ResponseWriter, r *http.Request) { - var v login + var v loginUser if err := s.decode(w, r, &v); err != nil { s.respond(w, r, err, http.StatusBadRequest) return @@ -106,59 +149,24 @@ func (s *Service) handleLogin(privateKey, publicKey jwk.Key) http.HandlerFunc { // -- Generate Tokens -- var now = time.Now().UTC() - var jwb = jwt.NewBuilder().Issuer("github.com/rog-golang-buddies/rmx").IssuedAt(now).Claim("email", u.Email) - // Audience([]string{"http://localhost:3000"}). - _ = jwb - - idToken, err := jwb.Subject(string(u.Email)).Expiration(now.Add(time.Hour * 10)).Build() + its, ats, rts, err := s.signedTokens(privateKey, now, string(u.Email), u.ID.String()) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) return } - idTokenSigned, err := jwt.Sign(idToken, jwt.WithKey(jwa.RS256, privateKey)) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - accessToken, err := jwt.NewBuilder().Subject(u.ID.String()).Expiration(now.Add(time.Minute * 5)).Build() - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - accessTokenSigned, err := jwt.Sign(accessToken, jwt.WithKey(jwa.RS256, privateKey)) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - refreshToken, err := jwt.NewBuilder().Subject(u.ID.String()).Expiration(now.Add(time.Hour * 24 * 7)).Build() - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - refreshTokenSigned, err := jwt.Sign(refreshToken, jwt.WithKey(jwa.RS256, privateKey)) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - // -- Generate Tokens -- - cookie := &http.Cookie{ Name: "RMX_DIRECT_RT", - Value: string(refreshTokenSigned), + Value: string(rts), HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteLaxMode, - Expires: time.Now().UTC().Add(time.Hour * 24 * 7), + Expires: now.Add(time.Hour * 24 * 7), } - var data = response{ - IDToken: string(idTokenSigned), - AccessToken: string(accessTokenSigned), + var data = authTokens{ + IDToken: string(its), + AccessToken: string(ats), } s.respondCookie(w, r, data, cookie) @@ -168,7 +176,6 @@ func (s *Service) handleLogin(privateKey, publicKey jwk.Key) http.HandlerFunc { func (s *Service) handleLogout() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cookie := &http.Cookie{ - // Path: "REFRESH_TOKEN_COOKIE_PATH", Name: "RMX_DIRECT_RT", Value: "", HttpOnly: true, @@ -217,7 +224,7 @@ func (s *Service) handleRefresh() http.HandlerFunc { } } -func (s *Service) authenticate(privateKey, publicKey jwk.Key) func(f http.Handler) http.Handler { +func (s *Service) authenticate(publicKey jwk.Key) func(f http.Handler) http.Handler { return func(f http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { token, err := jwt.ParseRequest(r, jwt.WithKey(jwa.RS256, publicKey), jwt.WithValidate(true)) @@ -232,6 +239,7 @@ func (s *Service) authenticate(privateKey, publicKey jwk.Key) func(f http.Handle return } + // Convert email from `string` type to `internal.Email` ? r = r.WithContext(context.WithValue(r.Context(), emailKey, email)) f.ServeHTTP(w, r) } From 515e66657fddff32b636160540459ea3712b042c Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 1 Oct 2022 23:00:32 +0100 Subject: [PATCH 104/246] in-mem secret will be replaced with env var --- service/user/routes.go | 2 -- service/user/routes_test.go | 8 ++++---- service/user/service.go | 14 +++++++------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/service/user/routes.go b/service/user/routes.go index a751a878..ba815eb3 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -76,8 +76,6 @@ func (s *Service) handleIdentity() http.HandlerFunc { } func (s *Service) signedTokens(key jwk.Key, now time.Time, email, uuid string) (its, ats, rts []byte, err error) { - // -- Generate Tokens -- - // var now = time.Now().UTC() var jwb = jwt.NewBuilder().Issuer("github.com/rog-golang-buddies/rmx").IssuedAt(now).Claim("email", email) // Audience([]string{"http://localhost:3000"}). diff --git a/service/user/routes_test.go b/service/user/routes_test.go index 62638ba6..0f19a752 100644 --- a/service/user/routes_test.go +++ b/service/user/routes_test.go @@ -43,6 +43,10 @@ func TestLogin(t *testing.T) { if err != nil { t.Fatal(err) } + if r.StatusCode != http.StatusOK { + t.Fatalf("expected %d; got %d", http.StatusOK, r.StatusCode) + } + defer r.Body.Close() type response struct { @@ -56,10 +60,6 @@ func TestLogin(t *testing.T) { t.Fatal(err) } - if r.StatusCode != http.StatusOK { - t.Fatalf("expected %d; got %d", http.StatusOK, r.StatusCode) - } - // get my user info req, _ := http.NewRequest(http.MethodGet, s.URL+"/api/v1/user/me", nil) req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, tokens.IDToken)) diff --git a/service/user/service.go b/service/user/service.go index dcd570de..e857ac0c 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -15,6 +15,7 @@ import ( "github.com/rog-golang-buddies/rmx/test/mock" ) +// TODO use os/viper to get `key.pem` body var secretTest = `-----BEGIN PRIVATE KEY----- MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAML5MHFgqUlZcENS hHZ83yXfoUpqaMfp5/UdgMIJ0S5DW5QEON6reAsDu6zP0BEVZhg65pEYWEraBrGK @@ -59,7 +60,6 @@ func NewService(m chi.Router, r internal.UserRepo) *Service { } func DefaultService() *Service { - s := &Service{chi.NewMux(), mock.UserRepo(), log.Default()} s.routes() return s @@ -87,7 +87,8 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, } func (s *Service) routes() { - // setup auth private/public key pair from key.pem + // panic should be ok as we need this to return no error + // else it'll completely break our auth model private, err := jwk.ParseKey([]byte(secretTest), jwk.WithPEM(true)) if err != nil { panic(err) @@ -97,11 +98,10 @@ func (s *Service) routes() { if err != nil { panic(err) } - // setup auth here s.m.Route("/api/v1/user", func(r chi.Router) { - auth := r.With(s.authenticate(private, public)) - auth.Get("/me", s.handleMyInfo()) + auth := r.With(s.authenticate(public)) + auth.Get("/me", s.handleIdentity()) r.Post("/", s.handleRegistration()) @@ -110,9 +110,9 @@ func (s *Service) routes() { }) s.m.Route("/api/v1/auth", func(r chi.Router) { - r.Post("/login", s.handleLogin(private, public)) + r.Post("/login", s.handleLogin(private)) - auth := r.With(s.authenticate(private, public)) + auth := r.With(s.authenticate(public)) auth.Get("/refresh", s.handleRefresh()) auth.Post("/logout", s.handleLogout()) }) From b21908bbcac84e473dc87b159453d914dc726b49 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 1 Oct 2022 23:20:15 +0100 Subject: [PATCH 105/246] small clean-up --- service/user/auth_test.go | 14 ------------ service/user/routes.go | 46 +++------------------------------------ service/user/service.go | 44 +++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 57 deletions(-) delete mode 100644 service/user/auth_test.go diff --git a/service/user/auth_test.go b/service/user/auth_test.go deleted file mode 100644 index f09a30c9..00000000 --- a/service/user/auth_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package user - -import "testing" - -var jwtKey = []byte("my_secret_key") - -var users = map[string]string{ - "user_1": "password_1", - "user_2": "password_2", -} - -func TestAuth(t *testing.T) { - -} diff --git a/service/user/routes.go b/service/user/routes.go index fa8e97c2..0a73ca88 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -75,48 +75,7 @@ func (s *Service) handleIdentity() http.HandlerFunc { } } -func (s *Service) signedTokens(key jwk.Key, now time.Time, email, uuid string) (its, ats, rts []byte, err error) { - var jwb = jwt.NewBuilder().Issuer("github.com/rog-golang-buddies/rmx").IssuedAt(now).Claim("email", email) - // Audience([]string{"http://localhost:3000"}). - - it, err := jwb.Subject(email).Expiration(now.Add(time.Hour * 10)).Build() - if err != nil { - return nil, nil, nil, err - - } - - its, err = jwt.Sign(it, jwt.WithKey(jwa.RS256, key)) - if err != nil { - return nil, nil, nil, err - - } - - at, err := jwt.NewBuilder().Subject(uuid).Expiration(now.Add(time.Minute * 5)).Build() - if err != nil { - return nil, nil, nil, err - } - - ats, err = jwt.Sign(at, jwt.WithKey(jwa.RS256, key)) - if err != nil { - return nil, nil, nil, err - - } - - rt, err := jwt.NewBuilder().Subject(uuid).Expiration(now.Add(time.Hour * 24 * 7)).Build() - if err != nil { - return nil, nil, nil, err - - } - - rts, err = jwt.Sign(rt, jwt.WithKey(jwa.RS256, key)) - if err != nil { - return nil, nil, nil, err - } - - return its, ats, rts, nil -} - -func (s *Service) handleLogin(privateKey jwk.Key) http.HandlerFunc { +func (s *Service) handleLogin(key jwk.Key) http.HandlerFunc { type loginUser struct { Email internal.Email `json:"email"` Password internal.Password `json:"password"` @@ -147,7 +106,7 @@ func (s *Service) handleLogin(privateKey jwk.Key) http.HandlerFunc { // -- Generate Tokens -- var now = time.Now().UTC() - its, ats, rts, err := s.signedTokens(privateKey, now, string(u.Email), u.ID.String()) + its, ats, rts, err := s.signedTokens(key, now, string(u.Email), u.ID.String()) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) return @@ -186,6 +145,7 @@ func (s *Service) handleLogout() http.HandlerFunc { } } +// still to develop func (s *Service) handleRefresh() http.HandlerFunc { type response struct { AccessToken string `json:"accessToken"` diff --git a/service/user/service.go b/service/user/service.go index e857ac0c..a8780ce7 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -4,9 +4,12 @@ import ( "errors" "log" "net/http" + "time" "github.com/go-chi/chi/v5" + "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" h "github.com/hyphengolang/prelude/http" @@ -86,6 +89,47 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, return suid.ParseString(chi.URLParam(r, "uuid")) } +func (s *Service) signedTokens(key jwk.Key, now time.Time, email, uuid string) (its, ats, rts []byte, err error) { + var jwb = jwt.NewBuilder().Issuer("github.com/rog-golang-buddies/rmx").IssuedAt(now).Claim("email", email) + // Audience([]string{"http://localhost:3000"}). + + it, err := jwb.Subject(email).Expiration(now.Add(time.Hour * 10)).Build() + if err != nil { + return nil, nil, nil, err + + } + + its, err = jwt.Sign(it, jwt.WithKey(jwa.RS256, key)) + if err != nil { + return nil, nil, nil, err + + } + + at, err := jwt.NewBuilder().Subject(uuid).Expiration(now.Add(time.Minute * 5)).Build() + if err != nil { + return nil, nil, nil, err + } + + ats, err = jwt.Sign(at, jwt.WithKey(jwa.RS256, key)) + if err != nil { + return nil, nil, nil, err + + } + + rt, err := jwt.NewBuilder().Subject(uuid).Expiration(now.Add(time.Hour * 24 * 7)).Build() + if err != nil { + return nil, nil, nil, err + + } + + rts, err = jwt.Sign(rt, jwt.WithKey(jwa.RS256, key)) + if err != nil { + return nil, nil, nil, err + } + + return its, ats, rts, nil +} + func (s *Service) routes() { // panic should be ok as we need this to return no error // else it'll completely break our auth model From 803d85d7fe52202e30bb65661fa6319eaac51b4d Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 1 Oct 2022 23:53:26 +0100 Subject: [PATCH 106/246] fixed token generation --- service/user/routes_test.go | 7 ++++--- service/user/service.go | 10 ++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/service/user/routes_test.go b/service/user/routes_test.go index 0f19a752..66bec82e 100644 --- a/service/user/routes_test.go +++ b/service/user/routes_test.go @@ -60,10 +60,11 @@ func TestLogin(t *testing.T) { t.Fatal(err) } - // get my user info - req, _ := http.NewRequest(http.MethodGet, s.URL+"/api/v1/user/me", nil) - req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, tokens.IDToken)) + t.Log(tokens) + // get my user info + req, _ := http.NewRequest(http.MethodGet, s.URL+"/api/v1/auth/me", nil) + req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, tokens.AccessToken)) r, err = s.Client().Do(req) if err != nil { t.Fatal(err) diff --git a/service/user/service.go b/service/user/service.go index a8780ce7..23178949 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -105,7 +105,7 @@ func (s *Service) signedTokens(key jwk.Key, now time.Time, email, uuid string) ( } - at, err := jwt.NewBuilder().Subject(uuid).Expiration(now.Add(time.Minute * 5)).Build() + at, err := jwb.Subject(uuid).Expiration(now.Add(time.Minute * 5)).Build() if err != nil { return nil, nil, nil, err } @@ -116,7 +116,7 @@ func (s *Service) signedTokens(key jwk.Key, now time.Time, email, uuid string) ( } - rt, err := jwt.NewBuilder().Subject(uuid).Expiration(now.Add(time.Hour * 24 * 7)).Build() + rt, err := jwb.Subject(uuid).Expiration(now.Add(time.Hour * 24 * 7)).Build() if err != nil { return nil, nil, nil, err @@ -138,15 +138,12 @@ func (s *Service) routes() { panic(err) } - public, err := jwk.PublicKeyOf(private) + public, err := private.PublicKey() if err != nil { panic(err) } s.m.Route("/api/v1/user", func(r chi.Router) { - auth := r.With(s.authenticate(public)) - auth.Get("/me", s.handleIdentity()) - r.Post("/", s.handleRegistration()) // health @@ -157,6 +154,7 @@ func (s *Service) routes() { r.Post("/login", s.handleLogin(private)) auth := r.With(s.authenticate(public)) + auth.Get("/me", s.handleIdentity()) auth.Get("/refresh", s.handleRefresh()) auth.Post("/logout", s.handleLogout()) }) From c112dee04ec2893ec3d893bc07f609de4f6cfd31 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sun, 2 Oct 2022 01:13:28 +0100 Subject: [PATCH 107/246] working on refresh route --- go.mod | 4 +++ go.sum | 15 +++++++++ internal/auth/auth.go | 68 +++++++++++++++++++++++++++++++++++++++++ service/user/routes.go | 57 +++++++++++----------------------- service/user/service.go | 35 ++++++++++++++++++--- 5 files changed, 134 insertions(+), 45 deletions(-) create mode 100644 internal/auth/auth.go diff --git a/go.mod b/go.mod index 238ec38d..d336945e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/hyphengolang/prelude v0.0.7 github.com/lithammer/shortuuid/v4 v4.0.0 + github.com/pkg/errors v0.9.1 github.com/rs/cors v1.8.2 github.com/urfave/cli/v2 v2.16.3 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e @@ -21,7 +22,9 @@ require ( ) require ( + github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/goccy/go-json v0.9.11 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect @@ -34,6 +37,7 @@ require ( github.com/BurntSushi/toml v1.1.0 // indirect github.com/containerd/console v1.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/go-redis/redis/v9 v9.0.0-beta.2 github.com/lestrrat-go/jwx/v2 v2.0.6 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.16 // indirect diff --git a/go.sum b/go.sum index a2af93e1..e2cd8a55 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og= github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4= @@ -20,12 +22,18 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-redis/redis/v9 v9.0.0-beta.2 h1:ZSr84TsnQyKMAg8gnV+oawuQezeJR11/09THcWCQzr4= +github.com/go-redis/redis/v9 v9.0.0-beta.2/go.mod h1:Bldcd/M/bm9HbnNPi/LUtYBSD8ttcZYBMupwMXhdU0o= github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -69,6 +77,11 @@ github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKt github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -97,6 +110,7 @@ golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/exp v0.0.0-20220921164117-439092de6870 h1:j8b6j9gzSigH28O5SjSpQSSh9lFd6f5D/q0aHjNTulc= golang.org/x/exp v0.0.0-20220921164117-439092de6870/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -118,6 +132,7 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 00000000..f9aafdce --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,68 @@ +package auth + +import ( + "context" + "time" + + "github.com/go-redis/redis/v9" + "github.com/pkg/errors" + "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/suid" +) + +var ErrNotImplemented = errors.New("not implemented") + +type Client struct { + rtdb, cidb *redis.Client +} + +func New(addr, password string) *Client { + rtdb := redis.Options{Addr: addr, Password: password, DB: 0} + cidb := redis.Options{Addr: addr, Password: password, DB: 1} + + c := &Client{redis.NewClient(&rtdb), redis.NewClient(&cidb)} + return c +} + +const ( + defaultAddr = "localhost:6379" + defaultPassword = "" +) + +var DefaultClient = &Client{ + rtdb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 0}), + cidb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 1}), +} + +func (c *Client) SetRefresh(ctx context.Context, token string, exp time.Duration) error { + + _, err := c.rtdb.Set(ctx, token, nil, exp).Result() + return err +} + +func (c *Client) HasTokenUsed(ctx context.Context, token string) bool { + // check if token is available in redis database + // if it's not then token is not reused + _, err := c.cidb.Get(ctx, token).Result() + return err == nil +} + +func (c *Client) SetClientID(ctx context.Context, id suid.UUID, email internal.Email, exp time.Duration) error { + _, err := c.cidb.Set(ctx, id.String(), email, exp).Result() + return err +} + +func (c *Client) HasClientID(ctx context.Context, id suid.UUID) bool { + // check if a key with client id exists + // if the key exists it means that the client id is revoked and token should be denied + // we don't need the email value here + _, err := c.cidb.Get(ctx, id.String()).Result() + return err == nil +} + +// isTokenUsed + +// saveRefreshToken +func (c *Client) SaveRefreshToken() error { + return ErrNotImplemented +} diff --git a/service/user/routes.go b/service/user/routes.go index 0a73ca88..4bda5689 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -9,7 +9,6 @@ import ( "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/suid" ) // var refreshTokenCookieName = "RMX_DIRECT_RT" @@ -22,31 +21,9 @@ import ( // 412 - Invalid precondition // 422 - Unprocessable -type signupUser struct { - Email internal.Email `json:"email"` - Username string `json:"username"` - Password internal.Password `json:"password"` -} - -func (v signupUser) decode(iu *internal.User) error { - h, err := v.Password.Hash() - if err != nil { - return err - } - - *iu = internal.User{ - ID: suid.NewUUID(), - Email: v.Email, - Username: v.Username, - Password: h, - } - - return nil -} - func (s *Service) handleRegistration() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var v signupUser + var v SignupUser if err := s.decode(w, r, &v); err != nil { s.respond(w, r, err, http.StatusBadRequest) return @@ -58,7 +35,7 @@ func (s *Service) handleRegistration() http.HandlerFunc { return } - if err := s.r.SignUp(u); err != nil { + if err := s.ur.SignUp(u); err != nil { s.respond(w, r, err, http.StatusInternalServerError) return } @@ -93,7 +70,7 @@ func (s *Service) handleLogin(key jwk.Key) http.HandlerFunc { return } - u, err := s.r.LookupEmail(v.Email) + u, err := s.ur.LookupEmail(v.Email) if err != nil { s.respond(w, r, err, http.StatusNotFound) return @@ -113,7 +90,7 @@ func (s *Service) handleLogin(key jwk.Key) http.HandlerFunc { } cookie := &http.Cookie{ - Name: "RMX_DIRECT_RT", + Name: cookieName, Value: string(rts), HttpOnly: true, Secure: r.TLS != nil, @@ -133,7 +110,7 @@ func (s *Service) handleLogin(key jwk.Key) http.HandlerFunc { func (s *Service) handleLogout() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cookie := &http.Cookie{ - Name: "RMX_DIRECT_RT", + Name: cookieName, Value: "", HttpOnly: true, Secure: r.TLS != nil, @@ -146,17 +123,13 @@ func (s *Service) handleLogout() http.HandlerFunc { } // still to develop -func (s *Service) handleRefresh() http.HandlerFunc { +func (s *Service) handleRefresh(privateKey jwk.Key) http.HandlerFunc { type response struct { AccessToken string `json:"accessToken"` } return func(w http.ResponseWriter, r *http.Request) { - c, err := r.Cookie("REFRESH_TOKEN_COOKIE_NAME") - if err != nil { - s.respond(w, r, err, http.StatusUnauthorized) - return - } + // email := r.Context().Value(emailKey).(string) /* Claims & Tokens @@ -164,10 +137,10 @@ func (s *Service) handleRefresh() http.HandlerFunc { Token generated by cookie value */ - *c = http.Cookie{ - Path: "REFRESH_TOKEN_COOKIE_PATH", - Name: "REFRESH_TOKEN_COOKIE_NAME", - Value: "GEN_REFRESH_TOKEN", + nc := &http.Cookie{ + // Path: "REFRESH_TOKEN_COOKIE_PATH", + Name: cookieName, + Value: "", HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteLaxMode, @@ -178,14 +151,14 @@ func (s *Service) handleRefresh() http.HandlerFunc { AccessToken: "GEN_ACCESS_TOKEN", } - s.respondCookie(w, r, data, c) + s.respondCookie(w, r, data, nc) } } func (s *Service) authenticate(publicKey jwk.Key) func(f http.Handler) http.Handler { return func(f http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - token, err := jwt.ParseRequest(r, jwt.WithKey(jwa.RS256, publicKey), jwt.WithValidate(true)) + token, err := jwt.ParseRequest(r, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey(cookieName), jwt.WithKey(jwa.RS256, publicKey), jwt.WithValidate(true)) if err != nil { s.respond(w, r, err, http.StatusUnauthorized) return @@ -205,3 +178,7 @@ func (s *Service) authenticate(publicKey jwk.Key) func(f http.Handler) http.Hand return http.HandlerFunc(fn) } } + +const ( + cookieName = "RMX_DIRECT_RT" +) diff --git a/service/user/service.go b/service/user/service.go index 23178949..a40f5f91 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -14,6 +14,7 @@ import ( h "github.com/hyphengolang/prelude/http" "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/auth" "github.com/rog-golang-buddies/rmx/internal/suid" "github.com/rog-golang-buddies/rmx/test/mock" ) @@ -48,22 +49,24 @@ var ( ) type Service struct { - m chi.Router - r internal.UserRepo + m chi.Router + ur internal.UserRepo l *log.Logger + + ac *auth.Client } func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } func NewService(m chi.Router, r internal.UserRepo) *Service { - s := &Service{m, r, log.Default()} + s := &Service{m: m, ur: r, l: log.Default()} s.routes() return s } func DefaultService() *Service { - s := &Service{chi.NewMux(), mock.UserRepo(), log.Default()} + s := &Service{m: chi.NewMux(), ur: mock.UserRepo(), l: log.Default()} s.routes() return s } @@ -155,7 +158,7 @@ func (s *Service) routes() { auth := r.With(s.authenticate(public)) auth.Get("/me", s.handleIdentity()) - auth.Get("/refresh", s.handleRefresh()) + auth.Get("/refresh", s.handleRefresh(private)) auth.Post("/logout", s.handleLogout()) }) } @@ -163,3 +166,25 @@ func (s *Service) routes() { func (s *Service) handlePing(w http.ResponseWriter, r *http.Request) { s.respond(w, r, nil, http.StatusNoContent) } + +type SignupUser struct { + Email internal.Email `json:"email"` + Username string `json:"username"` + Password internal.Password `json:"password"` +} + +func (v SignupUser) decode(iu *internal.User) error { + h, err := v.Password.Hash() + if err != nil { + return err + } + + *iu = internal.User{ + ID: suid.NewUUID(), + Email: v.Email, + Username: v.Username, + Password: h, + } + + return nil +} From 2ce24aa1076a2cea444b06c2f9ccb443d593a6a1 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sun, 2 Oct 2022 09:59:32 +0100 Subject: [PATCH 108/246] removed duplication with gen auth token --- internal/auth/auth.go | 38 +++++++++++++++++++++++++++++++++ internal/fp/fp.go | 8 +++++++ service/user/routes.go | 5 ++--- service/user/routes_test.go | 2 +- service/user/service.go | 42 ++++++++++++------------------------- 5 files changed, 62 insertions(+), 33 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index f9aafdce..413a3c4a 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -5,8 +5,12 @@ import ( "time" "github.com/go-redis/redis/v9" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" "github.com/pkg/errors" "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/suid" ) @@ -66,3 +70,37 @@ func (c *Client) HasClientID(ctx context.Context, id suid.UUID) bool { func (c *Client) SaveRefreshToken() error { return ErrNotImplemented } + +// -- Sign Tokens -- +func SignToken(key jwk.Key, opt *TokenOption) ([]byte, error) { + if !opt.Claim.HasValue() { + return nil, fp.ErrTuple + } + + var t time.Time + if opt.IssuedAt.IsZero() { + t = time.Now() + } else { + t = opt.IssuedAt + } + + token, _ := jwt.NewBuilder(). + Issuer(opt.Issuer). + Audience(opt.Audience). + Subject(opt.Subject). + Claim(opt.Claim[0], opt.Claim[1]). + IssuedAt(t). + Expiration(t.Add(opt.Expiration)). + Build() + + return jwt.Sign(token, jwt.WithKey(jwa.RS256, key)) +} + +type TokenOption struct { + Issuer string + Audience []string + Subject string + Claim fp.Tuple + IssuedAt time.Time + Expiration time.Duration +} diff --git a/internal/fp/fp.go b/internal/fp/fp.go index 6cdbeb77..2876925a 100644 --- a/internal/fp/fp.go +++ b/internal/fp/fp.go @@ -1,5 +1,7 @@ package fp +import "errors" + func FMap[T any, U any](vs []T, f func(T) U) (us []U) { us = make([]U, len(vs)) @@ -9,3 +11,9 @@ func FMap[T any, U any](vs []T, f func(T) U) (us []U) { return } + +var ErrTuple = errors.New(`"key/value" pair is missing "value"`) + +type Tuple [2]string + +func (t Tuple) HasValue() bool { return len(t) == 2 } diff --git a/service/user/routes.go b/service/user/routes.go index 4bda5689..4930e2fe 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -82,8 +82,7 @@ func (s *Service) handleLogin(key jwk.Key) http.HandlerFunc { } // -- Generate Tokens -- - var now = time.Now().UTC() - its, ats, rts, err := s.signedTokens(key, now, string(u.Email), u.ID.String()) + its, ats, rts, err := s.signedTokens(key, string(u.Email), u.ID.String()) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) return @@ -95,7 +94,7 @@ func (s *Service) handleLogin(key jwk.Key) http.HandlerFunc { HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteLaxMode, - Expires: now.Add(time.Hour * 24 * 7), + Expires: time.Now().UTC().Add(time.Hour * 24 * 7), } var data = authTokens{ diff --git a/service/user/routes_test.go b/service/user/routes_test.go index 66bec82e..1d4b4ee0 100644 --- a/service/user/routes_test.go +++ b/service/user/routes_test.go @@ -64,7 +64,7 @@ func TestLogin(t *testing.T) { // get my user info req, _ := http.NewRequest(http.MethodGet, s.URL+"/api/v1/auth/me", nil) - req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, tokens.AccessToken)) + req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, tokens.IDToken)) r, err = s.Client().Do(req) if err != nil { t.Fatal(err) diff --git a/service/user/service.go b/service/user/service.go index a40f5f91..8052779f 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -7,14 +7,13 @@ import ( "time" "github.com/go-chi/chi/v5" - "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" h "github.com/hyphengolang/prelude/http" "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/auth" + "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/suid" "github.com/rog-golang-buddies/rmx/test/mock" ) @@ -92,41 +91,26 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, return suid.ParseString(chi.URLParam(r, "uuid")) } -func (s *Service) signedTokens(key jwk.Key, now time.Time, email, uuid string) (its, ats, rts []byte, err error) { - var jwb = jwt.NewBuilder().Issuer("github.com/rog-golang-buddies/rmx").IssuedAt(now).Claim("email", email) - // Audience([]string{"http://localhost:3000"}). - - it, err := jwb.Subject(email).Expiration(now.Add(time.Hour * 10)).Build() - if err != nil { - return nil, nil, nil, err - - } - - its, err = jwt.Sign(it, jwt.WithKey(jwa.RS256, key)) - if err != nil { - return nil, nil, nil, err - +func (s *Service) signedTokens(key jwk.Key, email, uuid string) (its, ats, rts []byte, err error) { + opt := auth.TokenOption{ + Issuer: "github.com/rog-golang-buddies/rmx", + Subject: email, + Expiration: time.Hour * 10, + Claim: fp.Tuple{"email", email}, } - at, err := jwb.Subject(uuid).Expiration(now.Add(time.Minute * 5)).Build() - if err != nil { + if its, err = auth.SignToken(key, &opt); err != nil { return nil, nil, nil, err } - ats, err = jwt.Sign(at, jwt.WithKey(jwa.RS256, key)) - if err != nil { + opt.Subject = uuid + opt.Expiration = time.Minute * 5 + if ats, err = auth.SignToken(key, &opt); err != nil { return nil, nil, nil, err - } - rt, err := jwb.Subject(uuid).Expiration(now.Add(time.Hour * 24 * 7)).Build() - if err != nil { - return nil, nil, nil, err - - } - - rts, err = jwt.Sign(rt, jwt.WithKey(jwa.RS256, key)) - if err != nil { + opt.Expiration = time.Hour * 24 * 7 + if rts, err = auth.SignToken(key, &opt); err != nil { return nil, nil, nil, err } From a3d4c784230bb1638f146d024ba6696afdf1de6b Mon Sep 17 00:00:00 2001 From: adoublef Date: Sun, 2 Oct 2022 10:23:24 +0100 Subject: [PATCH 109/246] generate key from pem helper --- internal/auth/auth.go | 13 ++++++++++++- service/user/service.go | 9 ++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 413a3c4a..371925fb 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -71,7 +71,18 @@ func (c *Client) SaveRefreshToken() error { return ErrNotImplemented } -// -- Sign Tokens -- +func GenerateKeys(secret string) (private, public jwk.Key, err error) { + if private, err = jwk.ParseKey([]byte(secret), jwk.WithPEM(true)); err != nil { + return nil, nil, err + } + + if public, err = private.PublicKey(); err != nil { + return nil, nil, err + } + + return private, public, nil +} + func SignToken(key jwk.Key, opt *TokenOption) ([]byte, error) { if !opt.Claim.HasValue() { return nil, fp.ErrTuple diff --git a/service/user/service.go b/service/user/service.go index 8052779f..f9339b4a 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -15,7 +15,7 @@ import ( "github.com/rog-golang-buddies/rmx/internal/auth" "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/suid" - "github.com/rog-golang-buddies/rmx/test/mock" + "github.com/rog-golang-buddies/rmx/test/mock" // big no-no ) // TODO use os/viper to get `key.pem` body @@ -120,12 +120,7 @@ func (s *Service) signedTokens(key jwk.Key, email, uuid string) (its, ats, rts [ func (s *Service) routes() { // panic should be ok as we need this to return no error // else it'll completely break our auth model - private, err := jwk.ParseKey([]byte(secretTest), jwk.WithPEM(true)) - if err != nil { - panic(err) - } - - public, err := private.PublicKey() + private, public, err := auth.GenerateKeys(secretTest) if err != nil { panic(err) } From 6b42b85c8424e182959a0fbb1cb733a25bbeb6ab Mon Sep 17 00:00:00 2001 From: adoublef Date: Sun, 2 Oct 2022 20:37:37 +0100 Subject: [PATCH 110/246] added default run for debugging --- cmd/main.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/cmd/main.go b/cmd/main.go index 7da903bd..3c22b85d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -23,10 +23,58 @@ import ( func main() { if err := initCLI().Run(os.Args); err != nil { + // if err := defaultRun(); err != nil { log.Fatalln(err) } } +func defaultRun() error { + sCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + defer cancel() + + // ? should this defined within the instantiation of a new service + c := cors.Options{ + AllowedOrigins: []string{"*"}, // ? band-aid, needs to change to a flag + AllowCredentials: true, + AllowedMethods: []string{http.MethodGet, http.MethodPost}, + AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + } + + srv := http.Server{ + Addr: ":8080", + Handler: cors.New(c).Handler(service.Default()), + // max time to read request from the client + ReadTimeout: 10 * time.Second, + // max time to write response to the client + WriteTimeout: 10 * time.Second, + // max time for connections using TCP Keep-Alive + IdleTimeout: 120 * time.Second, + BaseContext: func(_ net.Listener) context.Context { return sCtx }, + ErrorLog: log.Default(), + } + + // srv.TLSConfig. + + g, gCtx := errgroup.WithContext(sCtx) + + g.Go(func() error { + // Run the server + srv.ErrorLog.Printf("App server starting on %s", srv.Addr) + return srv.ListenAndServe() + }) + + g.Go(func() error { + <-gCtx.Done() + return srv.Shutdown(context.Background()) + }) + + // if err := g.Wait(); err != nil { + // log.Printf("exit reason: %s \n", err) + // } + + return g.Wait() +} + func run(cfg *Config) error { sCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) defer cancel() @@ -52,6 +100,8 @@ func run(cfg *Config) error { ErrorLog: log.Default(), } + // srv.TLSConfig. + g, gCtx := errgroup.WithContext(sCtx) g.Go(func() error { From f2239f4530a2b25e5f8487a98a6185ac4136823f Mon Sep 17 00:00:00 2001 From: adoublef Date: Sun, 2 Oct 2022 20:37:56 +0100 Subject: [PATCH 111/246] attempt at reading PEM file. --- internal/auth/auth.go | 11 +++++++++++ internal/auth/jwt_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 371925fb..76499aec 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -2,6 +2,7 @@ package auth import ( "context" + "os" "time" "github.com/go-redis/redis/v9" @@ -71,6 +72,16 @@ func (c *Client) SaveRefreshToken() error { return ErrNotImplemented } +// would like to find an alternative to using `os` package +func LoadPEM(path string) (private, public jwk.Key, err error) { + buf, err := os.ReadFile(path) + if err != nil { + return + } + + return GenerateKeys(string(buf)) +} + func GenerateKeys(secret string) (private, public jwk.Key, err error) { if private, err = jwk.ParseKey([]byte(secret), jwk.WithPEM(true)); err != nil { return nil, nil, err diff --git a/internal/auth/jwt_test.go b/internal/auth/jwt_test.go index 1902b419..985ad78d 100644 --- a/internal/auth/jwt_test.go +++ b/internal/auth/jwt_test.go @@ -3,6 +3,7 @@ package auth import ( "fmt" "net/http" + "os" "testing" "github.com/lestrrat-go/jwx/v2/jwa" @@ -79,3 +80,26 @@ func TestJWT(t *testing.T) { _ = verifiedToken // -- Working with htt.Request -- } + +func TestCerts(t *testing.T) { + t.Parallel() + + // c := `../../certs/cert.pem` + k := `../../certs/key.pem` + buf, err := os.ReadFile(k) + if err != nil { + t.Fatalf("failed to read file %s\n", err) + } + + raw, _, err := jwk.DecodePEM(buf) + if err != nil { + t.Fatalf("failed to decode PEM key: %s\n", err) + } + + private, err := jwk.FromRaw(raw) + if err != nil { + t.Fatalf("failed to create private key") + } + + _ = private +} From 05e4fc65760fe65e2ac697b220f85efaa07f1c37 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sun, 2 Oct 2022 20:38:24 +0100 Subject: [PATCH 112/246] mock does signup new user properly now --- service/user/routes.go | 17 ++++++++++++----- service/user/routes_test.go | 6 +++--- service/user/service.go | 29 ++++++++++++++++++----------- test/mock/repo.go | 10 ++++++---- 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/service/user/routes.go b/service/user/routes.go index 4930e2fe..acacea5e 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -40,7 +40,7 @@ func (s *Service) handleRegistration() http.HandlerFunc { return } - s.created(w, r, u.ID.String()) + s.created(w, r, string(u.ID.ShortUUID())) } } @@ -48,11 +48,17 @@ func (s *Service) handleIdentity() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { email := r.Context().Value(emailKey).(string) - s.respond(w, r, email, http.StatusOK) + u, err := s.ur.LookupEmail(internal.Email(email)) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + s.respond(w, r, u, http.StatusOK) } } -func (s *Service) handleLogin(key jwk.Key) http.HandlerFunc { +func (s *Service) handleCreateSession(key jwk.Key) http.HandlerFunc { type loginUser struct { Email internal.Email `json:"email"` Password internal.Password `json:"password"` @@ -89,6 +95,7 @@ func (s *Service) handleLogin(key jwk.Key) http.HandlerFunc { } cookie := &http.Cookie{ + Path: "/user", Name: cookieName, Value: string(rts), HttpOnly: true, @@ -106,7 +113,7 @@ func (s *Service) handleLogin(key jwk.Key) http.HandlerFunc { } } -func (s *Service) handleLogout() http.HandlerFunc { +func (s *Service) handleDeleteSession() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cookie := &http.Cookie{ Name: cookieName, @@ -122,7 +129,7 @@ func (s *Service) handleLogout() http.HandlerFunc { } // still to develop -func (s *Service) handleRefresh(privateKey jwk.Key) http.HandlerFunc { +func (s *Service) handleRefreshSession(privateKey jwk.Key) http.HandlerFunc { type response struct { AccessToken string `json:"accessToken"` } diff --git a/service/user/routes_test.go b/service/user/routes_test.go index 1d4b4ee0..688c7b71 100644 --- a/service/user/routes_test.go +++ b/service/user/routes_test.go @@ -39,7 +39,7 @@ func TestLogin(t *testing.T) { "password":"difficultPassword" }` - r, err = s.Client().Post(s.URL+"/api/v1/auth/login", "application/json", strings.NewReader(payload)) + r, err = s.Client().Post(s.URL+"/api/v1/session", "application/json", strings.NewReader(payload)) if err != nil { t.Fatal(err) } @@ -63,7 +63,7 @@ func TestLogin(t *testing.T) { t.Log(tokens) // get my user info - req, _ := http.NewRequest(http.MethodGet, s.URL+"/api/v1/auth/me", nil) + req, _ := http.NewRequest(http.MethodGet, s.URL+"/api/v1/user/me", nil) req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, tokens.IDToken)) r, err = s.Client().Do(req) if err != nil { @@ -74,7 +74,7 @@ func TestLogin(t *testing.T) { t.Fatalf("expected %d; got %d", http.StatusOK, r.StatusCode) } - var str string + var str any if err := json.NewDecoder(r.Body).Decode(&str); err != nil { t.Fatal(err) } diff --git a/service/user/service.go b/service/user/service.go index f9339b4a..55ba09f8 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -125,27 +125,34 @@ func (s *Service) routes() { panic(err) } + // source: https://stackoverflow.com/questions/7140074/restfully-design-login-or-register-resources + + // [ ] GET /user/me ++ get my user details + // [ ] GET /user/:uuid ++ get user info + // [ ] GET /user ++ get all users + // [ ] POST /user ++ register new user + // [ ] POST /user/:uuid ++ update information about user + // [ ] DELETE /user ++ delete user from database + // [ ] GET /session ++ refresh session token + // [ ] POST /session ++ create session (due to logging in) + // [ ] DELETE /session ++ delete session (due to logging out) + s.m.Route("/api/v1/user", func(r chi.Router) { r.Post("/", s.handleRegistration()) - // health - r.Get("/ping", s.handlePing) + auth := r.With(s.authenticate(public)) + auth.Get("/me", s.handleIdentity()) }) - s.m.Route("/api/v1/auth", func(r chi.Router) { - r.Post("/login", s.handleLogin(private)) + s.m.Route("/api/v1/session", func(r chi.Router) { + r.Post("/", s.handleCreateSession(private)) auth := r.With(s.authenticate(public)) - auth.Get("/me", s.handleIdentity()) - auth.Get("/refresh", s.handleRefresh(private)) - auth.Post("/logout", s.handleLogout()) + auth.Get("/refresh", s.handleRefreshSession(private)) + auth.Post("/logout", s.handleDeleteSession()) }) } -func (s *Service) handlePing(w http.ResponseWriter, r *http.Request) { - s.respond(w, r, nil, http.StatusNoContent) -} - type SignupUser struct { Email internal.Email `json:"email"` Username string `json:"username"` diff --git a/test/mock/repo.go b/test/mock/repo.go index 6392443c..4c7c6379 100644 --- a/test/mock/repo.go +++ b/test/mock/repo.go @@ -64,11 +64,13 @@ func (r *userRepo) SignUp(u internal.User) error { r.mu.Lock() defer r.mu.Unlock() - if _, found := r.us[u.ID]; found { - return errExists - } else { - r.us[u.ID] = &u + for _, v := range r.us { + if v.Username == u.Username { + return errExists + } } + r.us[u.ID] = &u + return nil } From cde139fad3fa82f7f7d1bee5f0fe283d838e16bb Mon Sep 17 00:00:00 2001 From: adoublef Date: Sun, 2 Oct 2022 20:52:49 +0100 Subject: [PATCH 113/246] Cookie.secure set to false --- cmd/main.go | 5 ++--- service/user/routes.go | 9 ++++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 3c22b85d..a5e0f440 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,7 +5,6 @@ import ( "log" "net" "net/http" - "os" "os/signal" "strconv" "syscall" @@ -22,8 +21,8 @@ import ( // ) func main() { - if err := initCLI().Run(os.Args); err != nil { - // if err := defaultRun(); err != nil { + // if err := initCLI().Run(os.Args); err != nil { + if err := defaultRun(); err != nil { log.Fatalln(err) } } diff --git a/service/user/routes.go b/service/user/routes.go index acacea5e..418ad112 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -99,7 +99,8 @@ func (s *Service) handleCreateSession(key jwk.Key) http.HandlerFunc { Name: cookieName, Value: string(rts), HttpOnly: true, - Secure: r.TLS != nil, + Secure: false, + // Secure: r.TLS != nil, SameSite: http.SameSiteLaxMode, Expires: time.Now().UTC().Add(time.Hour * 24 * 7), } @@ -119,7 +120,8 @@ func (s *Service) handleDeleteSession() http.HandlerFunc { Name: cookieName, Value: "", HttpOnly: true, - Secure: r.TLS != nil, + Secure: false, + // Secure: r.TLS != nil, SameSite: http.SameSiteLaxMode, Expires: time.Unix(0, 0), } @@ -148,7 +150,8 @@ func (s *Service) handleRefreshSession(privateKey jwk.Key) http.HandlerFunc { Name: cookieName, Value: "", HttpOnly: true, - Secure: r.TLS != nil, + Secure: false, + // Secure: r.TLS != nil, SameSite: http.SameSiteLaxMode, Expires: time.Now().UTC().Add(time.Hour * 24 * 7), } From 5bdcc7d00196d88c9f6252b23f817acf91a9efe6 Mon Sep 17 00:00:00 2001 From: adoublef Date: Mon, 3 Oct 2022 01:23:56 +0100 Subject: [PATCH 114/246] delete session works --- cmd/main.go | 4 +-- internal/internal.go | 8 ++--- service/user/routes.go | 75 +++++++++++++++++++++++------------------ service/user/service.go | 28 +++++++++------ 4 files changed, 65 insertions(+), 50 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index a5e0f440..786ebf10 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -33,9 +33,9 @@ func defaultRun() error { // ? should this defined within the instantiation of a new service c := cors.Options{ - AllowedOrigins: []string{"*"}, // ? band-aid, needs to change to a flag + AllowedOrigins: []string{"http://localhost:8000"}, // ? band-aid, needs to change to a flag AllowCredentials: true, - AllowedMethods: []string{http.MethodGet, http.MethodPost}, + AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete}, AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, } diff --git a/internal/internal.go b/internal/internal.go index df8ae40f..bcb1efb6 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -17,10 +17,10 @@ type JamRepo interface { } type User struct { - ID suid.UUID - Email Email - Username string - Password PasswordHash + ID suid.UUID `json:"id"` + Email Email `json:"email"` + Username string `json:"username"` + Password PasswordHash `json:"-"` } type UserRepo interface { diff --git a/service/user/routes.go b/service/user/routes.go index 418ad112..fcd3a3fc 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -69,6 +69,8 @@ func (s *Service) handleCreateSession(key jwk.Key) http.HandlerFunc { AccessToken string `json:"accessToken"` } + var userPath = "/" + return func(w http.ResponseWriter, r *http.Request) { var v loginUser if err := s.decode(w, r, &v); err != nil { @@ -95,14 +97,14 @@ func (s *Service) handleCreateSession(key jwk.Key) http.HandlerFunc { } cookie := &http.Cookie{ - Path: "/user", + Path: userPath, Name: cookieName, Value: string(rts), HttpOnly: true, - Secure: false, - // Secure: r.TLS != nil, + // Secure: false, + Secure: r.TLS != nil, SameSite: http.SameSiteLaxMode, - Expires: time.Now().UTC().Add(time.Hour * 24 * 7), + MaxAge: int(time.Hour * 24 * 7), } var data = authTokens{ @@ -116,51 +118,62 @@ func (s *Service) handleCreateSession(key jwk.Key) http.HandlerFunc { func (s *Service) handleDeleteSession() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - cookie := &http.Cookie{ - Name: cookieName, - Value: "", + c := &http.Cookie{ + Path: "/", + Name: cookieName, + // Value: "", HttpOnly: true, - Secure: false, + // Secure: false, // Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - Expires: time.Unix(0, 0), + // SameSite: http.SameSiteLaxMode, + MaxAge: -1, } - s.respondCookie(w, r, nil, cookie) + s.respondCookie(w, r, http.StatusText(http.StatusOK), c) } } // still to develop -func (s *Service) handleRefreshSession(privateKey jwk.Key) http.HandlerFunc { +func (s *Service) handleRefreshSession(key jwk.Key) http.HandlerFunc { type response struct { AccessToken string `json:"accessToken"` } - return func(w http.ResponseWriter, r *http.Request) { - // email := r.Context().Value(emailKey).(string) + var userPath = "/" - /* - Claims & Tokens + return func(w http.ResponseWriter, r *http.Request) { + email := r.Context().Value(emailKey).(string) + u, _ := s.ur.LookupEmail(internal.Email(email)) - Token generated by cookie value - */ + _, ats, rts, err := s.signedTokens(key, email, u.ID.String()) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } - nc := &http.Cookie{ - // Path: "REFRESH_TOKEN_COOKIE_PATH", - Name: cookieName, - Value: "", - HttpOnly: true, - Secure: false, - // Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - Expires: time.Now().UTC().Add(time.Hour * 24 * 7), + // I should be able to assume that this exists, else just renew + var c *http.Cookie + if c, err = r.Cookie(cookieName); err != nil { + c = &http.Cookie{ + Path: userPath, + Name: cookieName, + Value: string(rts), + HttpOnly: true, + // Secure: false, // set to true in production + Secure: r.TLS != nil, + SameSite: http.SameSiteLaxMode, + MaxAge: int(time.Hour * 24 * 7), + } + } else { + c.MaxAge = int(time.Hour * 24 * 7) + c.Value = string(rts) } data := response{ - AccessToken: "GEN_ACCESS_TOKEN", + AccessToken: string(ats), } - s.respondCookie(w, r, data, nc) + s.respondCookie(w, r, data, c) } } @@ -187,7 +200,3 @@ func (s *Service) authenticate(publicKey jwk.Key) func(f http.Handler) http.Hand return http.HandlerFunc(fn) } } - -const ( - cookieName = "RMX_DIRECT_RT" -) diff --git a/service/user/service.go b/service/user/service.go index 55ba09f8..e9bc857f 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -18,6 +18,10 @@ import ( "github.com/rog-golang-buddies/rmx/test/mock" // big no-no ) +const ( + cookieName = "RMX_ACCESS_TOKEN" +) + // TODO use os/viper to get `key.pem` body var secretTest = `-----BEGIN PRIVATE KEY----- MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAML5MHFgqUlZcENS @@ -127,18 +131,20 @@ func (s *Service) routes() { // source: https://stackoverflow.com/questions/7140074/restfully-design-login-or-register-resources - // [ ] GET /user/me ++ get my user details - // [ ] GET /user/:uuid ++ get user info - // [ ] GET /user ++ get all users - // [ ] POST /user ++ register new user - // [ ] POST /user/:uuid ++ update information about user - // [ ] DELETE /user ++ delete user from database - // [ ] GET /session ++ refresh session token - // [ ] POST /session ++ create session (due to logging in) - // [ ] DELETE /session ++ delete session (due to logging out) + // [+] GET /user/me - get my user details + // [-] GET /user/{uuid} - get user info + // [-] GET /user - get all users + // [+] POST /user - register new user + // [ ] POST /user/{uuid} - update information about user + // [ ] DELETE /user - delete user from database + // [+] GET /session - refresh session token + // [+] POST /session - create session (due to logging in) + // [+] DELETE /session - delete session (due to logging out) s.m.Route("/api/v1/user", func(r chi.Router) { r.Post("/", s.handleRegistration()) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) }) + r.Get("/{uuid}", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) }) auth := r.With(s.authenticate(public)) auth.Get("/me", s.handleIdentity()) @@ -148,8 +154,8 @@ func (s *Service) routes() { r.Post("/", s.handleCreateSession(private)) auth := r.With(s.authenticate(public)) - auth.Get("/refresh", s.handleRefreshSession(private)) - auth.Post("/logout", s.handleDeleteSession()) + auth.Get("/", s.handleRefreshSession(private)) + auth.Delete("/", s.handleDeleteSession()) }) } From b8fb15f7c33dbecd6e7638fb59aacfbbb75f5e08 Mon Sep 17 00:00:00 2001 From: adoublef Date: Mon, 3 Oct 2022 10:51:27 +0100 Subject: [PATCH 115/246] removed logs --- service/user/routes_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/service/user/routes_test.go b/service/user/routes_test.go index 688c7b71..bbe44f29 100644 --- a/service/user/routes_test.go +++ b/service/user/routes_test.go @@ -60,8 +60,6 @@ func TestLogin(t *testing.T) { t.Fatal(err) } - t.Log(tokens) - // get my user info req, _ := http.NewRequest(http.MethodGet, s.URL+"/api/v1/user/me", nil) req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, tokens.IDToken)) @@ -78,8 +76,6 @@ func TestLogin(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&str); err != nil { t.Fatal(err) } - - t.Log(str) } func TestAuthMe(t *testing.T) { From 186ba5143a87a67b3e4eafa5f900b546a9e50688 Mon Sep 17 00:00:00 2001 From: adoublef Date: Mon, 3 Oct 2022 10:52:02 +0100 Subject: [PATCH 116/246] removed userPath variable for "/" --- service/user/routes.go | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/service/user/routes.go b/service/user/routes.go index fcd3a3fc..13e20d89 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -69,8 +69,6 @@ func (s *Service) handleCreateSession(key jwk.Key) http.HandlerFunc { AccessToken string `json:"accessToken"` } - var userPath = "/" - return func(w http.ResponseWriter, r *http.Request) { var v loginUser if err := s.decode(w, r, &v); err != nil { @@ -89,7 +87,6 @@ func (s *Service) handleCreateSession(key jwk.Key) http.HandlerFunc { return } - // -- Generate Tokens -- its, ats, rts, err := s.signedTokens(key, string(u.Email), u.ID.String()) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) @@ -97,7 +94,7 @@ func (s *Service) handleCreateSession(key jwk.Key) http.HandlerFunc { } cookie := &http.Cookie{ - Path: userPath, + Path: "/", Name: cookieName, Value: string(rts), HttpOnly: true, @@ -133,17 +130,15 @@ func (s *Service) handleDeleteSession() http.HandlerFunc { } } -// still to develop +// TODO still to develop func (s *Service) handleRefreshSession(key jwk.Key) http.HandlerFunc { type response struct { AccessToken string `json:"accessToken"` } - var userPath = "/" - return func(w http.ResponseWriter, r *http.Request) { email := r.Context().Value(emailKey).(string) - u, _ := s.ur.LookupEmail(internal.Email(email)) + u, _ := s.ur.LookupEmail(internal.Email(email)) // NOTE assuming this exists if auth checks passed _, ats, rts, err := s.signedTokens(key, email, u.ID.String()) if err != nil { @@ -151,11 +146,11 @@ func (s *Service) handleRefreshSession(key jwk.Key) http.HandlerFunc { return } - // I should be able to assume that this exists, else just renew + // NOTE I should be able to assume that this exists, else just renew var c *http.Cookie if c, err = r.Cookie(cookieName); err != nil { c = &http.Cookie{ - Path: userPath, + Path: "/", Name: cookieName, Value: string(rts), HttpOnly: true, @@ -178,9 +173,12 @@ func (s *Service) handleRefreshSession(key jwk.Key) http.HandlerFunc { } func (s *Service) authenticate(publicKey jwk.Key) func(f http.Handler) http.Handler { + // auth.Authenticate(s, publicKey, cookieName, emailKey) + return func(f http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - token, err := jwt.ParseRequest(r, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey(cookieName), jwt.WithKey(jwa.RS256, publicKey), jwt.WithValidate(true)) + // token, err := jwt.ParseRequest(r, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey(cookieName), jwt.WithKey(jwa.RS256, publicKey), jwt.WithValidate(true)) + token, err := jwt.ParseRequest(r, jwt.WithKey(jwa.RS256, publicKey)) if err != nil { s.respond(w, r, err, http.StatusUnauthorized) return @@ -192,7 +190,7 @@ func (s *Service) authenticate(publicKey jwk.Key) func(f http.Handler) http.Hand return } - // Convert email from `string` type to `internal.Email` ? + // NOTE convert email from `string` type to `internal.Email` ? r = r.WithContext(context.WithValue(r.Context(), emailKey, email)) f.ServeHTTP(w, r) } From 4cd02b1f219ec359bf9bd391eded79159c023d6e Mon Sep 17 00:00:00 2001 From: adoublef Date: Mon, 3 Oct 2022 10:53:03 +0100 Subject: [PATCH 117/246] user service comments pushed to top of routes method --- service/user/service.go | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/service/user/service.go b/service/user/service.go index e9bc857f..f45e483a 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -121,6 +121,17 @@ func (s *Service) signedTokens(key jwk.Key, email, uuid string) (its, ats, rts [ return its, ats, rts, nil } +// source: https://stackoverflow.com/questions/7140074/restfully-design-login-or-register-resources +// +// [+] GET /user/me - get my user details +// [-] GET /user/{uuid} - get user info +// [-] GET /user - get all users +// [+] POST /user - register new user +// [ ] POST /user/{uuid} - update information about user +// [ ] DELETE /user - delete user from database +// [+] GET /session - refresh session token +// [+] POST /session - create session (due to logging in) +// [+] DELETE /session - delete session (due to logging out) func (s *Service) routes() { // panic should be ok as we need this to return no error // else it'll completely break our auth model @@ -129,18 +140,6 @@ func (s *Service) routes() { panic(err) } - // source: https://stackoverflow.com/questions/7140074/restfully-design-login-or-register-resources - - // [+] GET /user/me - get my user details - // [-] GET /user/{uuid} - get user info - // [-] GET /user - get all users - // [+] POST /user - register new user - // [ ] POST /user/{uuid} - update information about user - // [ ] DELETE /user - delete user from database - // [+] GET /session - refresh session token - // [+] POST /session - create session (due to logging in) - // [+] DELETE /session - delete session (due to logging out) - s.m.Route("/api/v1/user", func(r chi.Router) { r.Post("/", s.handleRegistration()) r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) }) @@ -153,7 +152,7 @@ func (s *Service) routes() { s.m.Route("/api/v1/session", func(r chi.Router) { r.Post("/", s.handleCreateSession(private)) - auth := r.With(s.authenticate(public)) + auth := r.With(s.authenticate(public)) // may not need the key? auth.Get("/", s.handleRefreshSession(private)) auth.Delete("/", s.handleDeleteSession()) }) From 81a93e633fdaeb6b9843af05c0ad64e2a26fe01b Mon Sep 17 00:00:00 2001 From: pmoieni Date: Mon, 3 Oct 2022 20:31:13 +0330 Subject: [PATCH 118/246] fixed sqlc.yaml typo --- sqlc.yaml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/sqlc.yaml b/sqlc.yaml index aa3f21da..563aa656 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -1,15 +1,15 @@ -version: "2" +version: '2' sql: - - schema: "idb/schema/user_schema.sql" - queries: "db/schema/user_query.sql" - engine: "mysql" - gen: - go: - package: "user" - out: "db/user" - emit_db_tags: true - emit_prepared_queries: true - emit_empty_slices: true - emit_params_struct_pointers: true - emit_json_tags: true - json_tags_case_style: "camel" + - schema: 'db/schema/user_schema.sql' + queries: 'db/schema/user_query.sql' + engine: 'mysql' + gen: + go: + package: 'user' + out: 'db/user' + emit_db_tags: true + emit_prepared_queries: true + emit_empty_slices: true + emit_params_struct_pointers: true + emit_json_tags: true + json_tags_case_style: 'camel' From 792ace1a6a65da04379fdfabd35b7bd488ed099f Mon Sep 17 00:00:00 2001 From: pmoieni Date: Thu, 6 Oct 2022 12:15:55 +0330 Subject: [PATCH 119/246] better auth implementation (not tested) --- cmd/main.go | 20 +- internal/auth/auth.go | 128 ----------- internal/dto/dto.go | 119 ++++++++++ internal/internal.go | 82 +------ internal/internal_test.go | 16 +- internal/suid/suid.go | 2 + internal/websocket/client.go | 4 +- service/auth/routes.go | 196 +++++++++++++++++ service/auth/routes_test.go | 81 +++++++ service/auth/service.go | 159 +++++++++++++ service/internal/auth/auth.go | 208 ++++++++++++++++++ .../internal}/auth/jwt_test.go | 0 service/internal/middlewares/auth.go | 39 ++++ service/jam/routes.go | 7 +- service/service.go | 4 +- service/user/routes.go | 189 +--------------- service/user/routes_test.go | 75 +------ service/user/service.go | 113 +--------- sqlc.yaml | 6 +- {db => store/db}/schema/migration.sql | 2 +- {db => store/db}/schema/user_query.sql | 0 {db => store/db}/schema/user_schema.sql | 2 +- {db => store/db}/user/db.go | 0 {db => store/db}/user/models.go | 2 +- store/db/user/repo.go | 61 +++++ {db => store/db}/user/user_query.sql.go | 6 +- test/mock/repo.go | 28 ++- 27 files changed, 931 insertions(+), 618 deletions(-) delete mode 100644 internal/auth/auth.go create mode 100644 internal/dto/dto.go create mode 100644 service/auth/routes.go create mode 100644 service/auth/routes_test.go create mode 100644 service/auth/service.go create mode 100644 service/internal/auth/auth.go rename {internal => service/internal}/auth/jwt_test.go (100%) create mode 100644 service/internal/middlewares/auth.go rename {db => store/db}/schema/migration.sql (85%) rename {db => store/db}/schema/user_query.sql (100%) rename {db => store/db}/schema/user_schema.sql (85%) rename {db => store/db}/user/db.go (100%) rename {db => store/db}/user/models.go (91%) create mode 100644 store/db/user/repo.go rename {db => store/db}/user/user_query.sql.go (94%) diff --git a/cmd/main.go b/cmd/main.go index 786ebf10..734d1ce6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -28,12 +28,20 @@ func main() { } func defaultRun() error { - sCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + sCtx, cancel := signal.NotifyContext( + context.Background(), + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) defer cancel() // ? should this defined within the instantiation of a new service c := cors.Options{ - AllowedOrigins: []string{"http://localhost:8000"}, // ? band-aid, needs to change to a flag + AllowedOrigins: []string{ + "http://localhost:8000", + }, // ? band-aid, needs to change to a flag AllowCredentials: true, AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete}, AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, @@ -75,7 +83,13 @@ func defaultRun() error { } func run(cfg *Config) error { - sCtx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + sCtx, cancel := signal.NotifyContext( + context.Background(), + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) defer cancel() // ? should this defined within the instantiation of a new service diff --git a/internal/auth/auth.go b/internal/auth/auth.go deleted file mode 100644 index 76499aec..00000000 --- a/internal/auth/auth.go +++ /dev/null @@ -1,128 +0,0 @@ -package auth - -import ( - "context" - "os" - "time" - - "github.com/go-redis/redis/v9" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/pkg/errors" - "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/fp" - "github.com/rog-golang-buddies/rmx/internal/suid" -) - -var ErrNotImplemented = errors.New("not implemented") - -type Client struct { - rtdb, cidb *redis.Client -} - -func New(addr, password string) *Client { - rtdb := redis.Options{Addr: addr, Password: password, DB: 0} - cidb := redis.Options{Addr: addr, Password: password, DB: 1} - - c := &Client{redis.NewClient(&rtdb), redis.NewClient(&cidb)} - return c -} - -const ( - defaultAddr = "localhost:6379" - defaultPassword = "" -) - -var DefaultClient = &Client{ - rtdb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 0}), - cidb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 1}), -} - -func (c *Client) SetRefresh(ctx context.Context, token string, exp time.Duration) error { - - _, err := c.rtdb.Set(ctx, token, nil, exp).Result() - return err -} - -func (c *Client) HasTokenUsed(ctx context.Context, token string) bool { - // check if token is available in redis database - // if it's not then token is not reused - _, err := c.cidb.Get(ctx, token).Result() - return err == nil -} - -func (c *Client) SetClientID(ctx context.Context, id suid.UUID, email internal.Email, exp time.Duration) error { - _, err := c.cidb.Set(ctx, id.String(), email, exp).Result() - return err -} - -func (c *Client) HasClientID(ctx context.Context, id suid.UUID) bool { - // check if a key with client id exists - // if the key exists it means that the client id is revoked and token should be denied - // we don't need the email value here - _, err := c.cidb.Get(ctx, id.String()).Result() - return err == nil -} - -// isTokenUsed - -// saveRefreshToken -func (c *Client) SaveRefreshToken() error { - return ErrNotImplemented -} - -// would like to find an alternative to using `os` package -func LoadPEM(path string) (private, public jwk.Key, err error) { - buf, err := os.ReadFile(path) - if err != nil { - return - } - - return GenerateKeys(string(buf)) -} - -func GenerateKeys(secret string) (private, public jwk.Key, err error) { - if private, err = jwk.ParseKey([]byte(secret), jwk.WithPEM(true)); err != nil { - return nil, nil, err - } - - if public, err = private.PublicKey(); err != nil { - return nil, nil, err - } - - return private, public, nil -} - -func SignToken(key jwk.Key, opt *TokenOption) ([]byte, error) { - if !opt.Claim.HasValue() { - return nil, fp.ErrTuple - } - - var t time.Time - if opt.IssuedAt.IsZero() { - t = time.Now() - } else { - t = opt.IssuedAt - } - - token, _ := jwt.NewBuilder(). - Issuer(opt.Issuer). - Audience(opt.Audience). - Subject(opt.Subject). - Claim(opt.Claim[0], opt.Claim[1]). - IssuedAt(t). - Expiration(t.Add(opt.Expiration)). - Build() - - return jwt.Sign(token, jwt.WithKey(jwa.RS256, key)) -} - -type TokenOption struct { - Issuer string - Audience []string - Subject string - Claim fp.Tuple - IssuedAt time.Time - Expiration time.Duration -} diff --git a/internal/dto/dto.go b/internal/dto/dto.go new file mode 100644 index 00000000..1801c2c1 --- /dev/null +++ b/internal/dto/dto.go @@ -0,0 +1,119 @@ +package dto + +import ( + "errors" + "net/mail" + + "github.com/rog-golang-buddies/rmx/internal/suid" + gpv "github.com/wagslane/go-password-validator" + "golang.org/x/crypto/bcrypt" +) + +var ErrInvalidEmail = errors.New("rmx: invalid email") +var ErrNotImplemented = errors.New("rmx: not yet implemented") + +type JamRepo interface { +} + +type User struct { + ID suid.UUID `json:"id"` + Username string `json:"username"` + Email Email `json:"email"` + Password Password `json:"-"` +} + +func (u *User) HashPassword() error { + h, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + u.Password = Password(h) + return nil +} + +func (u *User) ComparePassword(p string) error { + return bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(p)) +} + +type UserRepo interface { + RUserRepo + WUserRepo +} + +type RUserRepo interface { + Lookup(uid *suid.UUID) (User, error) + LookupEmail(email string) (User, error) + ListAll() ([]User, error) +} + +type WUserRepo interface { + Add(u *User) error + Remove(uid *suid.UUID) error +} + +/* +var emailRegex = regexp.MustCompile( + `^[a-zA-Z0-9.!#$%&’*+/=?^_\x60{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$`, +) +*/ + +type Email string + +func (e *Email) String() string { return string(*e) } + +func (e *Email) Validate() error { + _, err := mail.ParseAddress(e.String()) + if err != nil { + return err + } + return nil +} + +/* +func (e *Email) UnmarshalJSON(b []byte) error { + if s := b[1 : len(b)-1]; emailRegex.Match(s) { + *e = Email(s) + return nil + } + + return ErrInvalidEmail +} +*/ + +// NOTE: better change to something in range 50-70 +const minEntropy float64 = 10.0 + +type Password string + +func (p *Password) String() string { return string(*p) } + +func (p *Password) Validate() error { + return gpv.Validate(p.String(), minEntropy) +} + +/* +func (p *Password) UnmarshalJSON(b []byte) error { + *p = Password(b[1 : len(b)-1]) + return gpv.Validate(string(*p), minEntropy) +} + +func (p *Password) MarshalJSON() (b []byte, err error) { + var sb strings.Builder + sb.WriteRune('"') + sb.WriteString(string(*p)) + sb.WriteRune('"') + return []byte(sb.String()), nil +} +*/ + +func (p *Password) Hash() (PasswordHash, error) { return newPasswordHash(*p) } + +type PasswordHash []byte + +func newPasswordHash(pw Password) (PasswordHash, error) { + return bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) +} + +func (pw *PasswordHash) Compare(cmp Password) error { + return bcrypt.CompareHashAndPassword(*pw, []byte(cmp)) +} diff --git a/internal/internal.go b/internal/internal.go index bcb1efb6..562c2338 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -1,43 +1,9 @@ package internal import ( - "errors" - "regexp" "strings" - - "github.com/rog-golang-buddies/rmx/internal/suid" - gpv "github.com/wagslane/go-password-validator" - "golang.org/x/crypto/bcrypt" ) -var ErrInvalidEmail = errors.New("rmx: invalid email") -var ErrNotImplemented = errors.New("rmx: not yet implemented") - -type JamRepo interface { -} - -type User struct { - ID suid.UUID `json:"id"` - Email Email `json:"email"` - Username string `json:"username"` - Password PasswordHash `json:"-"` -} - -type UserRepo interface { - RUserRepo - WUserRepo -} - -type RUserRepo interface { - Lookup(id suid.UUID) (*User, error) - LookupEmail(email Email) (*User, error) - ListAll() ([]*User, error) -} - -type WUserRepo interface { - SignUp(u User) error -} - type MsgTyp int const ( @@ -54,8 +20,8 @@ const ( NoteOff ) -func (t MsgTyp) String() string { - switch t { +func (t *MsgTyp) String() string { + switch *t { case Create: return "CREATE" case Delete: @@ -99,52 +65,10 @@ func (t *MsgTyp) UnmarshalJSON(b []byte) error { return nil } -func (t MsgTyp) MarshalJSON() ([]byte, error) { +func (t *MsgTyp) MarshalJSON() ([]byte, error) { var sb strings.Builder sb.WriteRune('"') sb.WriteString(t.String()) sb.WriteRune('"') return []byte(sb.String()), nil } - -var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9.!#$%&’*+/=?^_\x60{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$`) - -type Email string - -func (e *Email) UnmarshalJSON(b []byte) error { - if s := b[1 : len(b)-1]; emailRegex.Match(s) { - *e = Email(s) - return nil - } - - return ErrInvalidEmail -} - -const minEntropy float64 = 10.0 - -type Password string - -func (p *Password) UnmarshalJSON(b []byte) error { - *p = Password(b[1 : len(b)-1]) - return gpv.Validate(string(*p), minEntropy) -} - -func (p Password) MarshalJSON() (b []byte, err error) { - var sb strings.Builder - sb.WriteRune('"') - sb.WriteString(string(p)) - sb.WriteRune('"') - return []byte(sb.String()), nil -} - -func (p Password) Hash() (PasswordHash, error) { return newPasswordHash(p) } - -type PasswordHash []byte - -func newPasswordHash(pw Password) (PasswordHash, error) { - return bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) -} - -func (pw PasswordHash) Compare(cmp Password) error { - return bcrypt.CompareHashAndPassword(pw, []byte(cmp)) -} diff --git a/internal/internal_test.go b/internal/internal_test.go index 071fffda..eb34c565 100644 --- a/internal/internal_test.go +++ b/internal/internal_test.go @@ -1,23 +1,18 @@ package internal -import ( - "encoding/json" - "strings" - "testing" -) - +/* func TestPasswordType(t *testing.T) { t.Parallel() data := `"thispasswordiscomplex"` - var pw Password + var pw dto.Password err := json.NewDecoder(strings.NewReader(data)).Decode(&pw) if err != nil { t.Fatal(err) } - if exp := data[1 : len(data)-1]; pw != Password(exp) { + if exp := data[1 : len(data)-1]; pw != dto.Password(exp) { t.Errorf(`expected %s got %s`, exp, pw) } @@ -42,7 +37,7 @@ func TestEmailType(t *testing.T) { data := `"anon@gmail.com"` - var e Email + var e dto.Email err := json.NewDecoder(strings.NewReader(data)).Decode(&e) if err != nil { t.Fatal(err) @@ -63,6 +58,7 @@ func TestEmailType(t *testing.T) { err = json.NewDecoder(strings.NewReader(data)).Decode(&e) if err == nil { - t.Errorf("expected %v", ErrInvalidEmail) + t.Errorf("expected %v", dto.ErrInvalidEmail) } } +*/ diff --git a/internal/suid/suid.go b/internal/suid/suid.go index e19c5fce..c6b93c87 100644 --- a/internal/suid/suid.go +++ b/internal/suid/suid.go @@ -23,6 +23,8 @@ func ParseString(s string) (UUID, error) { return UUID{uid}, err } +func (s SUID) String() string { return string(s) } + func (s SUID) UUID() (UUID, error) { u, err := suid.DefaultEncoder.Decode(string(s)) return UUID{u}, err diff --git a/internal/websocket/client.go b/internal/websocket/client.go index a7126272..984b8554 100644 --- a/internal/websocket/client.go +++ b/internal/websocket/client.go @@ -4,7 +4,7 @@ import ( "log" "sync" - rmx "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/dto" "github.com/rog-golang-buddies/rmx/internal/suid" "golang.org/x/exp/maps" ) @@ -30,7 +30,7 @@ func NewClient() *Client { func (c *Client) Size() int { return len(c.ps) } func (c *Client) Close() error { - return rmx.ErrNotImplemented + return dto.ErrNotImplemented } func (c *Client) NewPool(maxCount int) (suid.UUID, error) { diff --git a/service/auth/routes.go b/service/auth/routes.go new file mode 100644 index 00000000..ec311a74 --- /dev/null +++ b/service/auth/routes.go @@ -0,0 +1,196 @@ +package auth + +import ( + "context" + "net/http" + + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/rog-golang-buddies/rmx/internal/dto" + "github.com/rog-golang-buddies/rmx/service/internal/auth" +) + +// Account Sign Up request handler +func (s *Service) handleSignUp() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var u dto.User + if err := s.decode(w, r, &u); err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + // check if the provided string is a valid email + if err := u.Email.Validate(); err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + // check if the password is strong enough + if err := u.Password.Validate(); err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + // hash the password to store in db + if err := u.HashPassword(); err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + if err := s.ur.Add(&u); err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + s.created(w, r, string(u.ID.ShortUUID())) + } +} + +// Account Sign In handler +func (s *Service) handleSignIn(key jwk.Key) http.HandlerFunc { + type loginUser struct { + Email dto.Email `json:"email"` + Password dto.Password `json:"password"` + } + + type authTokens struct { + IDToken string `json:"idToken"` + AccessToken string `json:"accessToken"` + } + + return func(w http.ResponseWriter, r *http.Request) { + var u loginUser + if err := s.decode(w, r, &u); err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + // Get users' information + ui, err := s.ur.LookupEmail(string(u.Email)) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + // Check if the provided password matches the users' + if err := ui.ComparePassword(string(u.Password)); err != nil { + s.respond(w, r, err, http.StatusUnauthorized) + return + } + + // Generate new JWT tokens + its, ats, rts, err := s.signedTokens(key, string(ui.Email), ui.ID.String()) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + // NOTE: Secure should be set to true in production + cookie := &http.Cookie{ + Path: "/", + Name: auth.RefreshTokenCookieName, + Value: string(rts), + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteLaxMode, + MaxAge: int(auth.RefreshTokenExpiry), + } + + v := &authTokens{ + IDToken: string(its), + AccessToken: string(ats), + } + + s.respondCookie(w, r, v, cookie) + } +} + +// Account Sign Out handler +// removes the Refresh Token by setting its MaxAge property to -1 +func (s *Service) handleSignOut() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // NOTE: Secure should be set to true in production + c := &http.Cookie{ + Path: "/", + Name: auth.RefreshTokenCookieName, + Value: "", + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteLaxMode, + MaxAge: -1, + } + + s.respondCookie(w, r, http.StatusText(http.StatusOK), c) + } +} + +func (s *Service) handleRefreshToken(key jwk.Key) http.HandlerFunc { + type response struct { + AccessToken string `json:"accessToken"` + } + + return func(w http.ResponseWriter, r *http.Request) { + rtc, err := r.Cookie(auth.RefreshTokenCookieName) + if err != nil { + s.respond(w, r, err, http.StatusUnauthorized) + return + } + + if err := s.arc.ValidateRefreshToken(context.Background(), rtc.Value); err != nil { + s.respond(w, r, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + } + + tc, err := auth.ParseRefreshTokenWithValidate(&key, rtc.Value) + if err != nil { + s.respond(w, r, err, http.StatusUnauthorized) + return + } + + email, ok := tc.PrivateClaims()["email"].(string) + if !ok { + s.respond(w, r, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + ui, err := s.ur.LookupEmail(email) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + _, ats, rts, err := s.signedTokens(key, email, ui.ID.String()) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + c := &http.Cookie{ + Path: "/", + Name: auth.RefreshTokenCookieName, + Value: string(rts), + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteLaxMode, + MaxAge: int(auth.RefreshTokenExpiry), + } + + v := &response{ + AccessToken: string(ats), + } + + s.respondCookie(w, r, v, c) + } +} + +func (s *Service) handleUserInfo() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + email := r.Context().Value(auth.EmailKey).(string) + + u, err := s.ur.LookupEmail(email) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + s.respond(w, r, u, http.StatusOK) + } +} diff --git a/service/auth/routes_test.go b/service/auth/routes_test.go new file mode 100644 index 00000000..7f831fa2 --- /dev/null +++ b/service/auth/routes_test.go @@ -0,0 +1,81 @@ +package auth + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestLogin(t *testing.T) { + srv := DefaultService() + + s := httptest.NewServer(srv) + t.Cleanup(func() { s.Close() }) + + // TODO add tests when - + // `password`, `email` or `username` is not present + payload := ` +{ + "username":"Test User", + "password":"difficultPassword", + "email":"user@gmail.com" +}` + + r, err := s.Client(). + Post(s.URL+"/api/v1/auth/register", "application/json", strings.NewReader(payload)) + if err != nil { + t.Fatal(err) + } + + if r.StatusCode != http.StatusCreated { + t.Fatalf("expected %d; got %d", http.StatusCreated, r.StatusCode) + } + + payload = ` +{ + "email":"user@gmail.com", + "password":"difficultPassword" +}` + + r, err = s.Client(). + Post(s.URL+"/api/v1/auth/login", "application/json", strings.NewReader(payload)) + if err != nil { + t.Fatal(err) + } + if r.StatusCode != http.StatusOK { + t.Fatalf("expected %d; got %d", http.StatusOK, r.StatusCode) + } + + defer r.Body.Close() + + type response struct { + IDToken string `json:"idToken"` + AccessToken string `json:"accessToken"` + } + + var tokens response + if err := json.NewDecoder(r.Body).Decode(&tokens); err != nil { + t.Fatal(err) + } + + /* + // get my user info + req, _ := http.NewRequest(http.MethodGet, s.URL+"/api/v1/account/me", nil) + req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, tokens.IDToken)) + r, err = s.Client().Do(req) + if err != nil { + t.Fatal(err) + } + + if r.StatusCode != http.StatusOK { + t.Fatalf("expected %d; got %d", http.StatusOK, r.StatusCode) + } + + var str any + if err := json.NewDecoder(r.Body).Decode(&str); err != nil { + t.Fatal(err) + } + */ +} diff --git a/service/auth/service.go b/service/auth/service.go new file mode 100644 index 00000000..5716c0b6 --- /dev/null +++ b/service/auth/service.go @@ -0,0 +1,159 @@ +package auth + +import ( + "log" + "net/http" + "time" + + h "github.com/hyphengolang/prelude/http" + "github.com/lestrrat-go/jwx/v2/jwk" + + "github.com/go-chi/chi/v5" + "github.com/rog-golang-buddies/rmx/internal/dto" + "github.com/rog-golang-buddies/rmx/internal/fp" + "github.com/rog-golang-buddies/rmx/internal/suid" + "github.com/rog-golang-buddies/rmx/service/internal/auth" + "github.com/rog-golang-buddies/rmx/service/internal/middlewares" + "github.com/rog-golang-buddies/rmx/test/mock" +) + +/* +// TODO use os/viper to get `key.pem` body +var secretTest = `-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAML5MHFgqUlZcENS +hHZ83yXfoUpqaMfp5/UdgMIJ0S5DW5QEON6reAsDu6zP0BEVZhg65pEYWEraBrGK +Vcbx7dsVqK4Z0GMm0YRAvB+1K+pYlXwld90mwG1TqOKDPQXqC0Z/jZi6DSsAhfJU +WN0rkInZRtoVeRzbbh+nLN8nd14fAgMBAAECgYEAor+A2VL3XBvFIt0RZxpq5mFa +cBSMrDsqfSeIX+/z5SsimVZA5lW5GXCfSuwY4Pm8xAL+jSUGJk0CA1bWrP8rLByS +cQAy1q0odaAiWIG5zFUEQBg5Q5b3+jXmh2zwtO7yhPuXn1/vBGg+FvyR57gV/3F+ +TuBfR6Bc3VWKuj7Gm5kCQQDuRgm8HTDbX7IQ0EFAVKB73Pj4Gx5u2NieD9U8+qXx +JsAdn1vRvQ3mNJDR5OcTr4rPkpLLCtzjA2iTDXp4yqmrAkEA0Xp91LCpImKAOtM3 +4SGXdzKi9+7fWmcTtfkz996y9A1C9l27Cj92P7OFdwMB4Z/ZMizJd0eXYhXr4IxH +wBoxXQJAUBOXp/HDfqZdiIsEsuL+AEKWJYOvqZ8UxaIajuDJrg7Q1+O7jvRTXH9k +ADZGdnYzV2kyDiy7aUu29Fy+QSQS+wJAJyEsdBhz35pqvZJK8+DkfD2XN50FV8u9 +YNamIH0XDIOVqJOlpqpoGkocejizl0PWvIqlL4TOAGJ75zwNAxNheQJABEA2/hfF +GMJsOrnD74rGP/Lfpg882AmeUoT5eH766sSobFfUYJZvyAmnQoK2Lzg2hrKwXXix +JvEGfrhihVLb7g== +-----END PRIVATE KEY----- +` +*/ + +type Service struct { + m chi.Router + ur dto.UserRepo + + l *log.Logger + + arc *auth.Client +} + +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } + +func NewService(m chi.Router, r dto.UserRepo) *Service { + s := &Service{m: m, ur: r, l: log.Default()} + s.routes() + return s +} + +func DefaultService() *Service { + s := &Service{m: chi.NewMux(), ur: mock.UserRepo(), l: log.Default()} + s.routes() + return s +} + +func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { + h.Respond(w, r, data, status) +} + +func (s *Service) respondCookie(w http.ResponseWriter, r *http.Request, data any, c *http.Cookie) { + http.SetCookie(w, c) + s.respond(w, r, data, http.StatusOK) +} + +func (s *Service) created(w http.ResponseWriter, r *http.Request, id string) { + h.Created(w, r, id) +} + +func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { + return h.Decode(w, r, data) +} + +func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { + return suid.ParseString(chi.URLParam(r, "uuid")) +} + +func (s *Service) signedTokens(key jwk.Key, email, uuid string) (its, ats, rts []byte, err error) { + // new client ID for tracking user connections + cid := suid.NewSUID() + opt := auth.TokenOption{ + Issuer: "github.com/rog-golang-buddies/rmx", + Subject: cid.String(), + Expiration: time.Hour * 10, + Claims: []fp.Tuple{{"email", email}}, + } + + if its, err = auth.SignToken(&key, &opt); err != nil { + return nil, nil, nil, err + } + + opt.Subject = uuid + opt.Expiration = auth.AccessTokenExpiry + if ats, err = auth.SignToken(&key, &opt); err != nil { + return nil, nil, nil, err + } + + opt.Expiration = auth.RefreshTokenExpiry + if rts, err = auth.SignToken(&key, &opt); err != nil { + return nil, nil, nil, err + } + + return its, ats, rts, nil +} + +/* +type SignupUser struct { + Email dto.Email `json:"email"` + Username string `json:"username"` + Password dto.Password `json:"password"` +} + +func (v *SignupUser) decode(iu *dto.User) error { + h, err := v.Password.Hash() + if err != nil { + return err + } + + *iu = dto.User{ + ID: suid.NewUUID(), + Email: v.Email, + Username: v.Username, + Password: h, + } + + return nil +} +*/ + +func (s *Service) routes() { + // initialize redis store + s.arc = auth.NewRedis("localhost:6379", "") + + // panic should be ok as we need this to return no error + // else it'll completely break our auth model + priv, pub, err := auth.GenerateKeys() + if err != nil { + s.l.Fatalln(err) + } + + s.m.Route("/api/v1/auth", func(r chi.Router) { + r.Post("/register", s.handleSignUp()) + r.Post("/login", s.handleSignIn(pub)) + r.Get("/refresh", s.handleRefreshToken(priv)) + r.Get("/logout", s.handleSignOut()) + }) + + s.m.Route("/api/v1/account", func(r chi.Router) { + r.Use(middlewares.Authenticate(pub)) + r.Get("/me", s.handleUserInfo()) + }) +} diff --git a/service/internal/auth/auth.go b/service/internal/auth/auth.go new file mode 100644 index 00000000..32c4069f --- /dev/null +++ b/service/internal/auth/auth.go @@ -0,0 +1,208 @@ +package auth + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "time" + + "github.com/go-redis/redis/v9" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/pkg/errors" + "github.com/rog-golang-buddies/rmx/internal/fp" +) + +type Client struct { + rtdb, cidb *redis.Client +} + +var ( + ErrNotImplemented = errors.New("not implemented") + ErrGenerateKey = errors.New("failed to generate new ecdsa key pair") + ErrSignTokens = errors.New("failed to generate signed tokens") + ErrRTValidate = errors.New("failed to validate refresh token") +) + +func NewRedis(addr, password string) *Client { + rtdb := redis.Options{Addr: addr, Password: password, DB: 0} + cidb := redis.Options{Addr: addr, Password: password, DB: 1} + + c := &Client{redis.NewClient(&rtdb), redis.NewClient(&cidb)} + return c +} + +const ( + defaultAddr = "localhost:6379" + defaultPassword = "" +) + +var DefaultClient = &Client{ + rtdb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 0}), + cidb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 1}), +} + +func (c *Client) ValidateRefreshToken(ctx context.Context, token string) error { + tc, err := ParseRefreshTokenClaims(token) + if err != nil { + return err + } + + cid := tc.Subject() + email, ok := tc.PrivateClaims()["email"].(string) + if !ok { + return ErrRTValidate + } + + if err := c.ValidateClientID(ctx, cid); err != nil { + return err + } + + if _, err := c.rtdb.Get(ctx, token).Result(); err != nil { + switch err { + case redis.Nil: + return nil + default: + return err + } + } + + err = c.RevokeClientID(ctx, cid, email) + if err != nil { + return err + } + + return ErrRTValidate +} + +func (c *Client) RevokeClientID(ctx context.Context, cid, email string) error { + _, err := c.cidb.Set(ctx, cid, email, RefreshTokenExpiry).Result() + if err != nil { + return err + } + return nil +} + +func (c *Client) RevokeRefreshToken(ctx context.Context, token string) error { + _, err := c.rtdb.Set(ctx, token, nil, RefreshTokenExpiry).Result() + return err +} + +func (c *Client) ValidateClientID(ctx context.Context, cid string) error { + // check if a key with client id exists + // if the key exists it means that the client id is revoked and token should be denied + // we don't need the email value here + _, err := c.cidb.Get(ctx, cid).Result() + if err != nil { + switch err { + case redis.Nil: + return nil + default: + return ErrRTValidate + } + } + + return ErrRTValidate +} + +/* +// would like to find an alternative to using `os` package +func LoadPEM(path string) (private, public jwk.Key, err error) { + buf, err := os.ReadFile(path) + if err != nil { + return + } + + return GenerateKeys(string(buf)) +} +*/ + +func GenerateKeys() (jwk.Key, jwk.Key, error) { + private, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + key, err := jwk.FromRaw(private) + if err != nil { + return nil, nil, err + } + + _, ok := key.(jwk.ECDSAPrivateKey) + if !ok { + return nil, nil, ErrGenerateKey + } + + pub, err := key.PublicKey() + if err != nil { + return nil, nil, err + } + + return key, pub, nil +} + +func SignToken(key *jwk.Key, opt *TokenOption) ([]byte, error) { + var t time.Time + if opt.IssuedAt.IsZero() { + t = time.Now().UTC() + } else { + t = opt.IssuedAt + } + + token, err := jwt.NewBuilder(). + Issuer(opt.Issuer). + Audience(opt.Audience). + Subject(opt.Subject). + IssuedAt(t). + Expiration(t.Add(opt.Expiration)). + Build() + if err != nil { + return nil, ErrSignTokens + } + + for _, c := range opt.Claims { + if !c.HasValue() { + return nil, fp.ErrTuple + } + + err := token.Set(c[0], c[1]) + if err != nil { + return nil, ErrSignTokens + } + } + + return jwt.Sign(token, jwt.WithKey(jwa.ES256, key)) +} + +func ParseRefreshTokenClaims(token string) (jwt.Token, error) { return jwt.Parse([]byte(token)) } + +func ParseRefreshTokenWithValidate(key *jwk.Key, token string) (jwt.Token, error) { + payload, err := jwt.Parse([]byte(token), + jwt.WithKey(jwa.ES256, key), + jwt.WithValidate(true)) + if err != nil { + return nil, err + } + + return payload, nil +} + +type TokenOption struct { + Issuer string + Audience []string + Subject string + Claims []fp.Tuple + IssuedAt time.Time + Expiration time.Duration +} + +type authCtxKey string + +const ( + RefreshTokenCookieName = "RMX_REFRESH_TOKEN" + RefreshTokenExpiry = time.Hour * 24 * 7 + AccessTokenExpiry = time.Minute * 5 + EmailKey = authCtxKey("rmx-email") +) diff --git a/internal/auth/jwt_test.go b/service/internal/auth/jwt_test.go similarity index 100% rename from internal/auth/jwt_test.go rename to service/internal/auth/jwt_test.go diff --git a/service/internal/middlewares/auth.go b/service/internal/middlewares/auth.go new file mode 100644 index 00000000..bf4b8ed3 --- /dev/null +++ b/service/internal/middlewares/auth.go @@ -0,0 +1,39 @@ +package middlewares + +import ( + "context" + "net/http" + + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" +) + +type contextKey string + +var emailKey = contextKey("rmx-email") + +func Authenticate(publicKey jwk.Key) func(f http.Handler) http.Handler { + return func(f http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + // token, err := jwt.ParseRequest(r, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey(cookieName), jwt.WithKey(jwa.RS256, publicKey), jwt.WithValidate(true)) + token, err := jwt.ParseRequest(r, jwt.WithKey(jwa.RS256, publicKey)) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + email, ok := token.PrivateClaims()["email"].(string) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + + // NOTE convert email from `string` type to `internal.Email` ? + r = r.WithContext(context.WithValue(r.Context(), emailKey, email)) + f.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) + } +} diff --git a/service/jam/routes.go b/service/jam/routes.go index 024e2154..1d1d762e 100644 --- a/service/jam/routes.go +++ b/service/jam/routes.go @@ -63,8 +63,11 @@ func (s *Service) handleListRooms() http.HandlerFunc { v := &response{ Sessions: fp.FMap(s.c.List(), func(p *ws.Pool) session { return session{ - ID: p.ID.ShortUUID(), - Users: fp.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), + ID: p.ID.ShortUUID(), + Users: fp.FMap( + p.Keys(), + func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }, + ), UserCount: p.Size(), } }), diff --git a/service/service.go b/service/service.go index a8707453..610bd71f 100644 --- a/service/service.go +++ b/service/service.go @@ -6,7 +6,7 @@ import ( "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" - "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/dto" "github.com/rog-golang-buddies/rmx/service/jam" "github.com/rog-golang-buddies/rmx/service/user" @@ -20,7 +20,7 @@ type Service struct { func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func New(m chi.Router, ur internal.UserRepo) *Service { +func New(m chi.Router, ur dto.UserRepo) *Service { s := &Service{m, log.Default()} // NOTE unsure how much is gained using a goroutine diff --git a/service/user/routes.go b/service/user/routes.go index 13e20d89..b4c47431 100644 --- a/service/user/routes.go +++ b/service/user/routes.go @@ -1,16 +1,5 @@ package user -import ( - "context" - "net/http" - "time" - - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/rog-golang-buddies/rmx/internal" -) - // var refreshTokenCookieName = "RMX_DIRECT_RT" // var refreshTokenCookiePath = "/api/v1" @@ -21,180 +10,4 @@ import ( // 412 - Invalid precondition // 422 - Unprocessable -func (s *Service) handleRegistration() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var v SignupUser - if err := s.decode(w, r, &v); err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - var u internal.User - if err := v.decode(&u); err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - if err := s.ur.SignUp(u); err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - s.created(w, r, string(u.ID.ShortUUID())) - } -} - -func (s *Service) handleIdentity() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - email := r.Context().Value(emailKey).(string) - - u, err := s.ur.LookupEmail(internal.Email(email)) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - s.respond(w, r, u, http.StatusOK) - } -} - -func (s *Service) handleCreateSession(key jwk.Key) http.HandlerFunc { - type loginUser struct { - Email internal.Email `json:"email"` - Password internal.Password `json:"password"` - } - - type authTokens struct { - IDToken string `json:"idToken"` - AccessToken string `json:"accessToken"` - } - - return func(w http.ResponseWriter, r *http.Request) { - var v loginUser - if err := s.decode(w, r, &v); err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - u, err := s.ur.LookupEmail(v.Email) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - if err := u.Password.Compare(v.Password); err != nil { - s.respond(w, r, err, http.StatusUnauthorized) - return - } - - its, ats, rts, err := s.signedTokens(key, string(u.Email), u.ID.String()) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - cookie := &http.Cookie{ - Path: "/", - Name: cookieName, - Value: string(rts), - HttpOnly: true, - // Secure: false, - Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - MaxAge: int(time.Hour * 24 * 7), - } - - var data = authTokens{ - IDToken: string(its), - AccessToken: string(ats), - } - - s.respondCookie(w, r, data, cookie) - } -} - -func (s *Service) handleDeleteSession() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - c := &http.Cookie{ - Path: "/", - Name: cookieName, - // Value: "", - HttpOnly: true, - // Secure: false, - // Secure: r.TLS != nil, - // SameSite: http.SameSiteLaxMode, - MaxAge: -1, - } - - s.respondCookie(w, r, http.StatusText(http.StatusOK), c) - } -} - -// TODO still to develop -func (s *Service) handleRefreshSession(key jwk.Key) http.HandlerFunc { - type response struct { - AccessToken string `json:"accessToken"` - } - - return func(w http.ResponseWriter, r *http.Request) { - email := r.Context().Value(emailKey).(string) - u, _ := s.ur.LookupEmail(internal.Email(email)) // NOTE assuming this exists if auth checks passed - - _, ats, rts, err := s.signedTokens(key, email, u.ID.String()) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - // NOTE I should be able to assume that this exists, else just renew - var c *http.Cookie - if c, err = r.Cookie(cookieName); err != nil { - c = &http.Cookie{ - Path: "/", - Name: cookieName, - Value: string(rts), - HttpOnly: true, - // Secure: false, // set to true in production - Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - MaxAge: int(time.Hour * 24 * 7), - } - } else { - c.MaxAge = int(time.Hour * 24 * 7) - c.Value = string(rts) - } - - data := response{ - AccessToken: string(ats), - } - - s.respondCookie(w, r, data, c) - } -} - -func (s *Service) authenticate(publicKey jwk.Key) func(f http.Handler) http.Handler { - // auth.Authenticate(s, publicKey, cookieName, emailKey) - - return func(f http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - // token, err := jwt.ParseRequest(r, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey(cookieName), jwt.WithKey(jwa.RS256, publicKey), jwt.WithValidate(true)) - token, err := jwt.ParseRequest(r, jwt.WithKey(jwa.RS256, publicKey)) - if err != nil { - s.respond(w, r, err, http.StatusUnauthorized) - return - } - - email, ok := token.PrivateClaims()["email"].(string) - if !ok { - s.respond(w, r, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - return - } - - // NOTE convert email from `string` type to `internal.Email` ? - r = r.WithContext(context.WithValue(r.Context(), emailKey, email)) - f.ServeHTTP(w, r) - } - - return http.HandlerFunc(fn) - } -} +// NOTE: UserInfo should be here diff --git a/service/user/routes_test.go b/service/user/routes_test.go index bbe44f29..d4a290e8 100644 --- a/service/user/routes_test.go +++ b/service/user/routes_test.go @@ -1,90 +1,18 @@ package user import ( - "encoding/json" - "fmt" "net/http" "net/http/httptest" - "strings" "testing" ) -func TestLogin(t *testing.T) { - srv := DefaultService() - - s := httptest.NewServer(srv) - t.Cleanup(func() { s.Close() }) - - // TODO add tests when - - // `password`, `email` or `username` is not present - payload := ` -{ - "username":"Test User", - "password":"difficultPassword", - "email":"user@gmail.com" -}` - - r, err := s.Client().Post(s.URL+"/api/v1/user", "application/json", strings.NewReader(payload)) - if err != nil { - t.Fatal(err) - } - - if r.StatusCode != http.StatusCreated { - t.Fatalf("expected %d; got %d", http.StatusCreated, r.StatusCode) - } - - payload = ` -{ - "email":"user@gmail.com", - "password":"difficultPassword" -}` - - r, err = s.Client().Post(s.URL+"/api/v1/session", "application/json", strings.NewReader(payload)) - if err != nil { - t.Fatal(err) - } - if r.StatusCode != http.StatusOK { - t.Fatalf("expected %d; got %d", http.StatusOK, r.StatusCode) - } - - defer r.Body.Close() - - type response struct { - IDToken string `json:"idToken"` - AccessToken string `json:"accessToken"` - // PublicKey string `json:"publicKey"` - } - - var tokens response - if err := json.NewDecoder(r.Body).Decode(&tokens); err != nil { - t.Fatal(err) - } - - // get my user info - req, _ := http.NewRequest(http.MethodGet, s.URL+"/api/v1/user/me", nil) - req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, tokens.IDToken)) - r, err = s.Client().Do(req) - if err != nil { - t.Fatal(err) - } - - if r.StatusCode != http.StatusOK { - t.Fatalf("expected %d; got %d", http.StatusOK, r.StatusCode) - } - - var str any - if err := json.NewDecoder(r.Body).Decode(&str); err != nil { - t.Fatal(err) - } -} - func TestAuthMe(t *testing.T) { srv := DefaultService() s := httptest.NewServer(srv) t.Cleanup(func() { s.Close() }) - r, err := s.Client().Get(s.URL + "/api/v1/user/me") + r, err := s.Client().Get(s.URL + "/api/v1/account/me") if err != nil { t.Fatal(err) } @@ -92,5 +20,4 @@ func TestAuthMe(t *testing.T) { if r.StatusCode != http.StatusUnauthorized { t.Fatalf("expected %d; got %d", http.StatusUnauthorized, r.StatusCode) } - } diff --git a/service/user/service.go b/service/user/service.go index f45e483a..1feaba79 100644 --- a/service/user/service.go +++ b/service/user/service.go @@ -4,47 +4,16 @@ import ( "errors" "log" "net/http" - "time" "github.com/go-chi/chi/v5" - "github.com/lestrrat-go/jwx/v2/jwk" h "github.com/hyphengolang/prelude/http" - "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/auth" - "github.com/rog-golang-buddies/rmx/internal/fp" + "github.com/rog-golang-buddies/rmx/internal/dto" "github.com/rog-golang-buddies/rmx/internal/suid" "github.com/rog-golang-buddies/rmx/test/mock" // big no-no ) -const ( - cookieName = "RMX_ACCESS_TOKEN" -) - -// TODO use os/viper to get `key.pem` body -var secretTest = `-----BEGIN PRIVATE KEY----- -MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAML5MHFgqUlZcENS -hHZ83yXfoUpqaMfp5/UdgMIJ0S5DW5QEON6reAsDu6zP0BEVZhg65pEYWEraBrGK -Vcbx7dsVqK4Z0GMm0YRAvB+1K+pYlXwld90mwG1TqOKDPQXqC0Z/jZi6DSsAhfJU -WN0rkInZRtoVeRzbbh+nLN8nd14fAgMBAAECgYEAor+A2VL3XBvFIt0RZxpq5mFa -cBSMrDsqfSeIX+/z5SsimVZA5lW5GXCfSuwY4Pm8xAL+jSUGJk0CA1bWrP8rLByS -cQAy1q0odaAiWIG5zFUEQBg5Q5b3+jXmh2zwtO7yhPuXn1/vBGg+FvyR57gV/3F+ -TuBfR6Bc3VWKuj7Gm5kCQQDuRgm8HTDbX7IQ0EFAVKB73Pj4Gx5u2NieD9U8+qXx -JsAdn1vRvQ3mNJDR5OcTr4rPkpLLCtzjA2iTDXp4yqmrAkEA0Xp91LCpImKAOtM3 -4SGXdzKi9+7fWmcTtfkz996y9A1C9l27Cj92P7OFdwMB4Z/ZMizJd0eXYhXr4IxH -wBoxXQJAUBOXp/HDfqZdiIsEsuL+AEKWJYOvqZ8UxaIajuDJrg7Q1+O7jvRTXH9k -ADZGdnYzV2kyDiy7aUu29Fy+QSQS+wJAJyEsdBhz35pqvZJK8+DkfD2XN50FV8u9 -YNamIH0XDIOVqJOlpqpoGkocejizl0PWvIqlL4TOAGJ75zwNAxNheQJABEA2/hfF -GMJsOrnD74rGP/Lfpg882AmeUoT5eH766sSobFfUYJZvyAmnQoK2Lzg2hrKwXXix -JvEGfrhihVLb7g== ------END PRIVATE KEY----- -` - -type contextKey string - -var emailKey = contextKey("rmx-email") - var ( ErrNoCookie = errors.New("user: cookie not found") ErrSessionNotFound = errors.New("user: session not found") @@ -53,16 +22,14 @@ var ( type Service struct { m chi.Router - ur internal.UserRepo + ur dto.UserRepo l *log.Logger - - ac *auth.Client } func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func NewService(m chi.Router, r internal.UserRepo) *Service { +func NewService(m chi.Router, r dto.UserRepo) *Service { s := &Service{m: m, ur: r, l: log.Default()} s.routes() return s @@ -95,32 +62,6 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, return suid.ParseString(chi.URLParam(r, "uuid")) } -func (s *Service) signedTokens(key jwk.Key, email, uuid string) (its, ats, rts []byte, err error) { - opt := auth.TokenOption{ - Issuer: "github.com/rog-golang-buddies/rmx", - Subject: email, - Expiration: time.Hour * 10, - Claim: fp.Tuple{"email", email}, - } - - if its, err = auth.SignToken(key, &opt); err != nil { - return nil, nil, nil, err - } - - opt.Subject = uuid - opt.Expiration = time.Minute * 5 - if ats, err = auth.SignToken(key, &opt); err != nil { - return nil, nil, nil, err - } - - opt.Expiration = time.Hour * 24 * 7 - if rts, err = auth.SignToken(key, &opt); err != nil { - return nil, nil, nil, err - } - - return its, ats, rts, nil -} - // source: https://stackoverflow.com/questions/7140074/restfully-design-login-or-register-resources // // [+] GET /user/me - get my user details @@ -132,50 +73,4 @@ func (s *Service) signedTokens(key jwk.Key, email, uuid string) (its, ats, rts [ // [+] GET /session - refresh session token // [+] POST /session - create session (due to logging in) // [+] DELETE /session - delete session (due to logging out) -func (s *Service) routes() { - // panic should be ok as we need this to return no error - // else it'll completely break our auth model - private, public, err := auth.GenerateKeys(secretTest) - if err != nil { - panic(err) - } - - s.m.Route("/api/v1/user", func(r chi.Router) { - r.Post("/", s.handleRegistration()) - r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) }) - r.Get("/{uuid}", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) }) - - auth := r.With(s.authenticate(public)) - auth.Get("/me", s.handleIdentity()) - }) - - s.m.Route("/api/v1/session", func(r chi.Router) { - r.Post("/", s.handleCreateSession(private)) - - auth := r.With(s.authenticate(public)) // may not need the key? - auth.Get("/", s.handleRefreshSession(private)) - auth.Delete("/", s.handleDeleteSession()) - }) -} - -type SignupUser struct { - Email internal.Email `json:"email"` - Username string `json:"username"` - Password internal.Password `json:"password"` -} - -func (v SignupUser) decode(iu *internal.User) error { - h, err := v.Password.Hash() - if err != nil { - return err - } - - *iu = internal.User{ - ID: suid.NewUUID(), - Email: v.Email, - Username: v.Username, - Password: h, - } - - return nil -} +func (s *Service) routes() {} diff --git a/sqlc.yaml b/sqlc.yaml index 563aa656..9075e5b3 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -1,12 +1,12 @@ version: '2' sql: - - schema: 'db/schema/user_schema.sql' - queries: 'db/schema/user_query.sql' + - schema: 'store/db/schema/user_schema.sql' + queries: 'store/db/schema/user_query.sql' engine: 'mysql' gen: go: package: 'user' - out: 'db/user' + out: 'store/db/user' emit_db_tags: true emit_prepared_queries: true emit_empty_slices: true diff --git a/db/schema/migration.sql b/store/db/schema/migration.sql similarity index 85% rename from db/schema/migration.sql rename to store/db/schema/migration.sql index b610edfe..062cb892 100644 --- a/db/schema/migration.sql +++ b/store/db/schema/migration.sql @@ -1,6 +1,6 @@ -- +migrate UP CREATE TABLE users ( - id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + id text NOT NULL PRIMARY KEY, username text NOT NULL, email text NOT NULL, password text NOT NULL, diff --git a/db/schema/user_query.sql b/store/db/schema/user_query.sql similarity index 100% rename from db/schema/user_query.sql rename to store/db/schema/user_query.sql diff --git a/db/schema/user_schema.sql b/store/db/schema/user_schema.sql similarity index 85% rename from db/schema/user_schema.sql rename to store/db/schema/user_schema.sql index 1d115ea2..4e80e1f5 100644 --- a/db/schema/user_schema.sql +++ b/store/db/schema/user_schema.sql @@ -1,5 +1,5 @@ CREATE TABLE users ( - id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + id text NOT NULL PRIMARY KEY, username text NOT NULL, email text NOT NULL, password text NOT NULL, diff --git a/db/user/db.go b/store/db/user/db.go similarity index 100% rename from db/user/db.go rename to store/db/user/db.go diff --git a/db/user/models.go b/store/db/user/models.go similarity index 91% rename from db/user/models.go rename to store/db/user/models.go index e7e131ac..956d9dd7 100644 --- a/db/user/models.go +++ b/store/db/user/models.go @@ -10,7 +10,7 @@ import ( ) type User struct { - ID int64 `db:"id" json:"id"` + ID string `db:"id" json:"id"` Username string `db:"username" json:"username"` Email string `db:"email" json:"email"` Password string `db:"password" json:"password"` diff --git a/store/db/user/repo.go b/store/db/user/repo.go new file mode 100644 index 00000000..ec2c1240 --- /dev/null +++ b/store/db/user/repo.go @@ -0,0 +1,61 @@ +package user + +import ( + "context" + "database/sql" + "errors" + + "github.com/rog-golang-buddies/rmx/internal/suid" +) + +type userRepo struct { + DBConn *sql.DB +} + +var ( + errTodo = errors.New("not yet implemented") + errNotFound = errors.New("user not found") + errExists = errors.New("user already exists") +) + +func UserRepo(c *sql.DB) *userRepo { + r := &userRepo{ + DBConn: c, + } + return r +} + +func (r *userRepo) ListAll() ([]User, error) { + ctx := context.Background() + q := New(r.DBConn) + return q.ListUsers(ctx) +} + +func (r *userRepo) Lookup(uid *suid.UUID) (User, error) { + ctx := context.Background() + q := New(r.DBConn) + return q.GetUserByID(ctx, uid.String()) +} + +func (r *userRepo) LookupEmail(email string) (User, error) { + ctx := context.Background() + q := New(r.DBConn) + return q.GetUserByEmail(ctx, email) +} + +func (r *userRepo) Add(u *CreateUserParams) error { + ctx := context.Background() + q := New(r.DBConn) + _, err := q.CreateUser(ctx, u) + if err != nil { + return err + } + + return nil +} + +func (r *userRepo) Remove(uid *suid.UUID) error { + ctx := context.Background() + q := New(r.DBConn) + return q.DeleteUser(ctx, uid.String()) +} diff --git a/db/user/user_query.sql.go b/store/db/user/user_query.sql.go similarity index 94% rename from db/user/user_query.sql.go rename to store/db/user/user_query.sql.go index a8903f4f..cfa6637c 100644 --- a/db/user/user_query.sql.go +++ b/store/db/user/user_query.sql.go @@ -48,7 +48,7 @@ DELETE FROM users WHERE id = ? ` -func (q *Queries) DeleteUser(ctx context.Context, id int64) error { +func (q *Queries) DeleteUser(ctx context.Context, id string) error { _, err := q.exec(ctx, q.deleteUserStmt, deleteUser, id) return err } @@ -82,7 +82,7 @@ WHERE id = ? LIMIT 1 ` -func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) { +func (q *Queries) GetUserByID(ctx context.Context, id string) (User, error) { row := q.queryRow(ctx, q.getUserByIDStmt, getUserByID, id) var i User err := row.Scan( @@ -142,7 +142,7 @@ UPDATE users type UpdateUserParams struct { Username string `db:"username" json:"username"` - ID int64 `db:"id" json:"id"` + ID string `db:"id" json:"id"` } func (q *Queries) UpdateUser(ctx context.Context, arg *UpdateUserParams) (sql.Result, error) { diff --git a/test/mock/repo.go b/test/mock/repo.go index 4c7c6379..e4c1f511 100644 --- a/test/mock/repo.go +++ b/test/mock/repo.go @@ -4,7 +4,7 @@ import ( "errors" "sync" - "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/dto" "github.com/rog-golang-buddies/rmx/internal/suid" "golang.org/x/exp/maps" ) @@ -17,50 +17,50 @@ var ( type userRepo struct { mu sync.Mutex - us map[suid.UUID]*internal.User + us map[suid.UUID]dto.User } func UserRepo() *userRepo { r := &userRepo{ - us: map[suid.UUID]*internal.User{}, + us: map[suid.UUID]dto.User{}, } return r } -func (r *userRepo) ListAll() ([]*internal.User, error) { +func (r *userRepo) ListAll() ([]dto.User, error) { r.mu.Lock() defer r.mu.Unlock() return maps.Values(r.us), nil } -func (r *userRepo) Lookup(uid suid.UUID) (*internal.User, error) { +func (r *userRepo) Lookup(uid *suid.UUID) (dto.User, error) { r.mu.Lock() defer r.mu.Unlock() for _, u := range r.us { - if u.ID == uid { + if &u.ID == uid { return u, nil } } - return nil, errNotFound + return dto.User{}, errNotFound } -func (r *userRepo) LookupEmail(email internal.Email) (*internal.User, error) { +func (r *userRepo) LookupEmail(email string) (dto.User, error) { r.mu.Lock() defer r.mu.Unlock() for _, u := range r.us { - if u.Email == email { + if string(u.Email) == email { return u, nil } } - return nil, errNotFound + return dto.User{}, errNotFound } -func (r *userRepo) SignUp(u internal.User) error { +func (r *userRepo) Add(u *dto.User) error { r.mu.Lock() defer r.mu.Unlock() @@ -70,7 +70,11 @@ func (r *userRepo) SignUp(u internal.User) error { } } - r.us[u.ID] = &u + r.us[u.ID] = *u return nil } + +func (r *userRepo) Remove(uid *suid.UUID) error { + return nil +} From 133d9ea2200dca45a551a12471ac2c43a0ba15e1 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 09:49:33 +0100 Subject: [PATCH 120/246] included testing package --- internal/is/io.go | 112 +++++++++++++++++++++++++++++++++ internal/is/is.go | 157 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 internal/is/io.go create mode 100644 internal/is/is.go diff --git a/internal/is/io.go b/internal/is/io.go new file mode 100644 index 00000000..86c774de --- /dev/null +++ b/internal/is/io.go @@ -0,0 +1,112 @@ +package is + +import ( + "bufio" + "os" + "regexp" + "runtime" + "strings" +) + +const maxStackLen = 50 + +var reIsSourceFile = regexp.MustCompile(`is(-1.7)?\.go$`) + +func (is *I) callerInfo() (path string, line int, ok bool) { + var pc [maxStackLen]uintptr + // Skip two extra frames to account for this function + // and runtime.Callers itself. + n := runtime.Callers(2, pc[:]) + if n == 0 { + panic("is: zero callers found") + } + frames := runtime.CallersFrames(pc[:n]) + var firstFrame, frame runtime.Frame + for more := true; more; { + frame, more = frames.Next() + if reIsSourceFile.MatchString(frame.File) { + continue + } + if firstFrame.PC == 0 { + firstFrame = frame + } + if _, ok := is.helpers[frame.Function]; ok { + // Frame is inside a helper function. + continue + } + return frame.File, frame.Line, true + } + // If no "non-helper" frame is found, the first non is frame is returned. + return firstFrame.File, firstFrame.Line, true +} + +// loadArguments gets the arguments from the function call +// on the specified line of the file. +func loadArguments(path string, line int) (string, bool) { + f, err := os.Open(path) + if err != nil { + return "", false + } + defer f.Close() + s := bufio.NewScanner(f) + i := 1 + for s.Scan() { + if i != line { + i++ + continue + } + text := s.Text() + braceI := strings.Index(text, "(") + if braceI == -1 { + return "", false + } + text = text[braceI+1:] + cs := bufio.NewScanner(strings.NewReader(text)) + cs.Split(bufio.ScanBytes) + j := 0 + c := 1 + for cs.Scan() { + switch cs.Text() { + case ")": + c-- + case "(": + c++ + } + if c == 0 { + break + } + j++ + } + text = text[:j] + return text, true + } + return "", false +} + +// loadComment gets the Go comment from the specified line +// in the specified file. +func loadComment(path string, line int) (string, bool) { + f, err := os.Open(path) + if err != nil { + return "", false + } + defer f.Close() + s := bufio.NewScanner(f) + i := 1 + for s.Scan() { + if i != line { + i++ + continue + } + + text := s.Text() + commentI := strings.Index(text, "// ") + if commentI == -1 { + return "", false // no comment + } + text = text[commentI+2:] + text = strings.TrimSpace(text) + return text, true + } + return "", false +} diff --git a/internal/is/is.go b/internal/is/is.go new file mode 100644 index 00000000..48dcb007 --- /dev/null +++ b/internal/is/is.go @@ -0,0 +1,157 @@ +// NOTE the original always breaks for +// me so have taken one part +// of this, hopefully that is ok +// source: https://github.com/matryer/is + +package is + +import ( + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "strings" +) + +type T interface { + Fail() + FailNow() +} + +// I is the test helper harness. +type I struct { + t T + out io.Writer + fail func() + helpers map[string]struct{} +} + +func New(t T) *I { + return &I{t, os.Stdout, t.FailNow, map[string]struct{}{}} +} + +func (is *I) NoErr(err error) { + if err != nil { + is.logf("err: %s", err.Error()) + } +} + +func (is *I) True(expression bool) { + if !expression { + is.log("not true: $ARGS") + } +} + +func (is *I) Equal(a, b any) { + if areEqual(a, b) { + return + } + // NOTE source: https://github.com/matryer/is/issues/46 + if isNil(a) || isNil(b) || reflect.ValueOf(a).Type() != reflect.ValueOf(b).Type() { + is.logf("%s != %s", is.valWithType(a), is.valWithType(b)) + } else { + is.logf("%v != %v", a, b) + } +} + +func (is *I) logf(format string, args ...interface{}) { + is.log(fmt.Sprintf(format, args...)) +} + +func (is *I) log(args ...interface{}) { + s := is.decorate(fmt.Sprint(args...)) + fmt.Fprint(os.Stdout, s) + is.fail() +} + +func (is *I) valWithType(v interface{}) string { + if isNil(v) { + return "" + } + return fmt.Sprintf("%[1]T(%[1]v)", v) +} + +// isNil gets whether the object is nil or not. +func isNil(object interface{}) bool { + if object == nil { + return true + } + value := reflect.ValueOf(object) + kind := value.Kind() + if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() { + return true + } + return false +} + +// areEqual gets whether a equals b or not. +func areEqual(a, b interface{}) bool { + // NOTE source: https://github.com/matryer/is/issues/49 + if isNil(a) || isNil(b) { + return isNil(a) && isNil(b) + } + + if reflect.DeepEqual(a, b) { + return true + } + aValue := reflect.ValueOf(a) + bValue := reflect.ValueOf(b) + return aValue == bValue +} + +// TODO add line and position number - https://github.com/matryer/is/blob/master/is.go +// +// decorate prefixes the string with the file and line of the call site +// and inserts the final newline if needed and indentation tabs for formatting. +// this function was copied from the testing framework and modified. +func (is *I) decorate(s string) string { + path, lineNumber, ok := is.callerInfo() // decorate + log + public function. + file := filepath.Base(path) + if ok { + // Truncate file name at last file name separator. + if index := strings.LastIndex(file, "/"); index >= 0 { + file = file[index+1:] + } else if index = strings.LastIndex(file, "\\"); index >= 0 { + file = file[index+1:] + } + } else { + file = "???" + lineNumber = 1 + } + + var sb strings.Builder + fmt.Fprintf(&sb, "%s:%d: ", file, lineNumber) // avoids needing to use strconv + s = escapeFormatString(s) + + lines := strings.Split(s, "\n") + if l := len(lines); l > 1 && lines[l-1] == "" { + lines = lines[:l-1] + } + + for i, line := range lines { + if i > 0 { + // Second and subsequent lines are indented an extra tab. + sb.WriteString("\n\t\t") + } + // expand arguments (if $ARGS is present) + if strings.Contains(line, "$ARGS") { + args, _ := loadArguments(path, lineNumber) + line = strings.Replace(line, "$ARGS", args, -1) + } + sb.WriteString(line) + } + comment, ok := loadComment(path, lineNumber) + if ok { + sb.WriteString(" // ") + comment = escapeFormatString(comment) + sb.WriteString(comment) + } + sb.WriteRune('\n') + return sb.String() +} + +// escapeFormatString escapes strings for use in formatted functions like Sprintf. +func escapeFormatString(fmt string) string { + return strings.Replace(fmt, "%", "%%", -1) +} From aa924c8124684487fb50d57c034cafbcacbc10f5 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 09:50:11 +0100 Subject: [PATCH 121/246] service.Service should not use go routines --- service/service.go | 8 ++++---- service/service_test.go | 24 ------------------------ 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/service/service.go b/service/service.go index 610bd71f..82697079 100644 --- a/service/service.go +++ b/service/service.go @@ -23,12 +23,12 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeH func New(m chi.Router, ur dto.UserRepo) *Service { s := &Service{m, log.Default()} + s.m.Use(middleware.Logger) + // NOTE unsure how much is gained using a goroutine // will have to investigate - go jam.NewService(s.m) - go user.NewService(s.m, ur) - - s.m.Use(middleware.Logger) + jam.NewService(s.m) + user.NewService(s.m, ur) return s } diff --git a/service/service_test.go b/service/service_test.go index 89ec7828..0bf2db16 100644 --- a/service/service_test.go +++ b/service/service_test.go @@ -1,33 +1,9 @@ package service import ( - "net/http/httptest" "testing" ) func TestIntegration(t *testing.T) { - srv := Default() - s := httptest.NewServer(srv) - t.Cleanup(func() { s.Close() }) - - // jam service - r, err := s.Client().Get(s.URL + "/api/v1/jam/ping") - if err != nil { - t.Fatal(err) - } - - if r.StatusCode != 204 { - t.Fatalf("expected %d;got %d", 204, r.StatusCode) - } - - // user service - r, err = s.Client().Get(s.URL + "/api/v1/user/ping") - if err != nil { - t.Fatal(err) - } - - if r.StatusCode != 204 { - t.Fatalf("expected %d;got %d", 204, r.StatusCode) - } } From 1635c31be0a52414aa934e33288947c98561d505 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 09:57:25 +0100 Subject: [PATCH 122/246] root service is currently disconnected from cmd/ --- cmd/main.go | 2 +- service/service.go | 28 +++++++++++----------------- store/store.go | 10 ++++++++++ 3 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 store/store.go diff --git a/cmd/main.go b/cmd/main.go index 734d1ce6..168a5990 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -49,7 +49,7 @@ func defaultRun() error { srv := http.Server{ Addr: ":8080", - Handler: cors.New(c).Handler(service.Default()), + Handler: cors.New(c).Handler(http.DefaultServeMux), // max time to read request from the client ReadTimeout: 10 * time.Second, // max time to write response to the client diff --git a/service/service.go b/service/service.go index 82697079..6760e8e7 100644 --- a/service/service.go +++ b/service/service.go @@ -6,32 +6,26 @@ import ( "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" - "github.com/rog-golang-buddies/rmx/internal/dto" - "github.com/rog-golang-buddies/rmx/service/jam" - "github.com/rog-golang-buddies/rmx/service/user" - - "github.com/rog-golang-buddies/rmx/test/mock" + "github.com/rog-golang-buddies/rmx/store" ) +func (s *Service) routes() { + s.m.Use(middleware.Logger) +} + type Service struct { m chi.Router + l *log.Logger } func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func New(m chi.Router, ur dto.UserRepo) *Service { - s := &Service{m, log.Default()} +func New(st store.Store) *Service { + s := &Service{chi.NewMux(), log.Default()} + s.routes() - s.m.Use(middleware.Logger) - - // NOTE unsure how much is gained using a goroutine - // will have to investigate - jam.NewService(s.m) - user.NewService(s.m, ur) + // jam.NewService(s.m) + // user.NewService(s.m, ur) return s } - -func Default() *Service { - return New(chi.NewMux(), mock.UserRepo()) -} diff --git a/store/store.go b/store/store.go new file mode 100644 index 00000000..97078565 --- /dev/null +++ b/store/store.go @@ -0,0 +1,10 @@ +package store + +import "context" + +type Store struct{} + +func New(ctx context.Context) *Store { + db := &Store{} + return db +} From 4d0e5f4282eb3a5c6fb9191bf425cb63b9b3b185 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Thu, 6 Oct 2022 12:28:19 +0330 Subject: [PATCH 123/246] fix authenticate middleware (not tested) --- service/internal/middlewares/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/internal/middlewares/auth.go b/service/internal/middlewares/auth.go index bf4b8ed3..bc43cb82 100644 --- a/service/internal/middlewares/auth.go +++ b/service/internal/middlewares/auth.go @@ -17,7 +17,7 @@ func Authenticate(publicKey jwk.Key) func(f http.Handler) http.Handler { return func(f http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { // token, err := jwt.ParseRequest(r, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey(cookieName), jwt.WithKey(jwa.RS256, publicKey), jwt.WithValidate(true)) - token, err := jwt.ParseRequest(r, jwt.WithKey(jwa.RS256, publicKey)) + token, err := jwt.ParseRequest(r, jwt.WithKey(jwa.ES256, publicKey)) if err != nil { w.WriteHeader(http.StatusUnauthorized) return From d62380aff34d6ef0891397db3c0e918424e1abc4 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 10:04:43 +0100 Subject: [PATCH 124/246] delete service tests, will re-add --- service/auth/routes_test.go | 81 ------------------------------------- service/auth/v2/service.go | 62 ++++++++++++++++++++++++++++ service/service_test.go | 9 ----- service/user/routes.go | 13 ------ service/user/routes_test.go | 23 ----------- service/user/service.go | 76 ---------------------------------- 6 files changed, 62 insertions(+), 202 deletions(-) delete mode 100644 service/auth/routes_test.go create mode 100644 service/auth/v2/service.go delete mode 100644 service/service_test.go delete mode 100644 service/user/routes.go delete mode 100644 service/user/routes_test.go delete mode 100644 service/user/service.go diff --git a/service/auth/routes_test.go b/service/auth/routes_test.go deleted file mode 100644 index 7f831fa2..00000000 --- a/service/auth/routes_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package auth - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" -) - -func TestLogin(t *testing.T) { - srv := DefaultService() - - s := httptest.NewServer(srv) - t.Cleanup(func() { s.Close() }) - - // TODO add tests when - - // `password`, `email` or `username` is not present - payload := ` -{ - "username":"Test User", - "password":"difficultPassword", - "email":"user@gmail.com" -}` - - r, err := s.Client(). - Post(s.URL+"/api/v1/auth/register", "application/json", strings.NewReader(payload)) - if err != nil { - t.Fatal(err) - } - - if r.StatusCode != http.StatusCreated { - t.Fatalf("expected %d; got %d", http.StatusCreated, r.StatusCode) - } - - payload = ` -{ - "email":"user@gmail.com", - "password":"difficultPassword" -}` - - r, err = s.Client(). - Post(s.URL+"/api/v1/auth/login", "application/json", strings.NewReader(payload)) - if err != nil { - t.Fatal(err) - } - if r.StatusCode != http.StatusOK { - t.Fatalf("expected %d; got %d", http.StatusOK, r.StatusCode) - } - - defer r.Body.Close() - - type response struct { - IDToken string `json:"idToken"` - AccessToken string `json:"accessToken"` - } - - var tokens response - if err := json.NewDecoder(r.Body).Decode(&tokens); err != nil { - t.Fatal(err) - } - - /* - // get my user info - req, _ := http.NewRequest(http.MethodGet, s.URL+"/api/v1/account/me", nil) - req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, tokens.IDToken)) - r, err = s.Client().Do(req) - if err != nil { - t.Fatal(err) - } - - if r.StatusCode != http.StatusOK { - t.Fatalf("expected %d; got %d", http.StatusOK, r.StatusCode) - } - - var str any - if err := json.NewDecoder(r.Body).Decode(&str); err != nil { - t.Fatal(err) - } - */ -} diff --git a/service/auth/v2/service.go b/service/auth/v2/service.go new file mode 100644 index 00000000..ee08ca70 --- /dev/null +++ b/service/auth/v2/service.go @@ -0,0 +1,62 @@ +package auth + +import ( + "context" + "errors" + "log" + "net/http" + + "github.com/go-chi/chi/v5" + + h "github.com/hyphengolang/prelude/http" + + "github.com/rog-golang-buddies/rmx/internal/dto" + "github.com/rog-golang-buddies/rmx/internal/suid" + // big no-no +) + +var ( + ErrNoCookie = errors.New("user: cookie not found") + ErrSessionNotFound = errors.New("user: session not found") + ErrSessionExists = errors.New("user: session already exists") +) + +func (s *Service) routes() {} + +type Service struct { + ctx context.Context + + m chi.Router + + log func(...any) + logf func(string, ...any) + + decode func(http.ResponseWriter, *http.Request, any) error + respond func(http.ResponseWriter, *http.Request, any, int) + created func(http.ResponseWriter, *http.Request, string) + setCookie func(http.ResponseWriter, *http.Cookie) +} + +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } + +func NewService(ctx context.Context, m chi.Router, r dto.UserRepo) *Service { + s := &Service{ + ctx, + m, + + log.Println, + log.Printf, + + h.Decode, + h.Respond, + h.Created, + http.SetCookie, + } + + s.routes() + return s +} + +func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { + return suid.ParseString(chi.URLParam(r, "uuid")) +} diff --git a/service/service_test.go b/service/service_test.go deleted file mode 100644 index 0bf2db16..00000000 --- a/service/service_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package service - -import ( - "testing" -) - -func TestIntegration(t *testing.T) { - -} diff --git a/service/user/routes.go b/service/user/routes.go deleted file mode 100644 index b4c47431..00000000 --- a/service/user/routes.go +++ /dev/null @@ -1,13 +0,0 @@ -package user - -// var refreshTokenCookieName = "RMX_DIRECT_RT" -// var refreshTokenCookiePath = "/api/v1" - -// 400 - catch all -// 401 - unauthorized -// 403 - Forbidden -// 409 - Conflict (details already exist) -// 412 - Invalid precondition -// 422 - Unprocessable - -// NOTE: UserInfo should be here diff --git a/service/user/routes_test.go b/service/user/routes_test.go deleted file mode 100644 index d4a290e8..00000000 --- a/service/user/routes_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package user - -import ( - "net/http" - "net/http/httptest" - "testing" -) - -func TestAuthMe(t *testing.T) { - srv := DefaultService() - - s := httptest.NewServer(srv) - t.Cleanup(func() { s.Close() }) - - r, err := s.Client().Get(s.URL + "/api/v1/account/me") - if err != nil { - t.Fatal(err) - } - - if r.StatusCode != http.StatusUnauthorized { - t.Fatalf("expected %d; got %d", http.StatusUnauthorized, r.StatusCode) - } -} diff --git a/service/user/service.go b/service/user/service.go deleted file mode 100644 index 1feaba79..00000000 --- a/service/user/service.go +++ /dev/null @@ -1,76 +0,0 @@ -package user - -import ( - "errors" - "log" - "net/http" - - "github.com/go-chi/chi/v5" - - h "github.com/hyphengolang/prelude/http" - - "github.com/rog-golang-buddies/rmx/internal/dto" - "github.com/rog-golang-buddies/rmx/internal/suid" - "github.com/rog-golang-buddies/rmx/test/mock" // big no-no -) - -var ( - ErrNoCookie = errors.New("user: cookie not found") - ErrSessionNotFound = errors.New("user: session not found") - ErrSessionExists = errors.New("user: session already exists") -) - -type Service struct { - m chi.Router - ur dto.UserRepo - - l *log.Logger -} - -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } - -func NewService(m chi.Router, r dto.UserRepo) *Service { - s := &Service{m: m, ur: r, l: log.Default()} - s.routes() - return s -} - -func DefaultService() *Service { - s := &Service{m: chi.NewMux(), ur: mock.UserRepo(), l: log.Default()} - s.routes() - return s -} - -func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { - h.Respond(w, r, data, status) -} - -func (s *Service) respondCookie(w http.ResponseWriter, r *http.Request, data any, c *http.Cookie) { - http.SetCookie(w, c) - s.respond(w, r, data, http.StatusOK) -} - -func (s *Service) created(w http.ResponseWriter, r *http.Request, id string) { - h.Created(w, r, id) -} - -func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { - return h.Decode(w, r, data) -} - -func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { - return suid.ParseString(chi.URLParam(r, "uuid")) -} - -// source: https://stackoverflow.com/questions/7140074/restfully-design-login-or-register-resources -// -// [+] GET /user/me - get my user details -// [-] GET /user/{uuid} - get user info -// [-] GET /user - get all users -// [+] POST /user - register new user -// [ ] POST /user/{uuid} - update information about user -// [ ] DELETE /user - delete user from database -// [+] GET /session - refresh session token -// [+] POST /session - create session (due to logging in) -// [+] DELETE /session - delete session (due to logging out) -func (s *Service) routes() {} From 5804a825dc410aec686106a18e236aa0dd1bd65d Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 10:06:34 +0100 Subject: [PATCH 125/246] auth back in internal --- {service/internal => internal}/auth/auth.go | 30 ++++++++++++++ .../internal => internal}/auth/jwt_test.go | 0 service/internal/middlewares/auth.go | 39 ------------------- 3 files changed, 30 insertions(+), 39 deletions(-) rename {service/internal => internal}/auth/auth.go (83%) rename {service/internal => internal}/auth/jwt_test.go (100%) delete mode 100644 service/internal/middlewares/auth.go diff --git a/service/internal/auth/auth.go b/internal/auth/auth.go similarity index 83% rename from service/internal/auth/auth.go rename to internal/auth/auth.go index 32c4069f..7a688c7b 100644 --- a/service/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "net/http" "time" "github.com/go-redis/redis/v9" @@ -206,3 +207,32 @@ const ( AccessTokenExpiry = time.Minute * 5 EmailKey = authCtxKey("rmx-email") ) + +type contextKey string + +var emailKey = contextKey("rmx-email") + +func Authenticate(publicKey jwk.Key) func(f http.Handler) http.Handler { + return func(f http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + // token, err := jwt.ParseRequest(r, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey(cookieName), jwt.WithKey(jwa.RS256, publicKey), jwt.WithValidate(true)) + token, err := jwt.ParseRequest(r, jwt.WithKey(jwa.ES256, publicKey)) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + email, ok := token.PrivateClaims()["email"].(string) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + + // NOTE convert email from `string` type to `internal.Email` ? + r = r.WithContext(context.WithValue(r.Context(), emailKey, email)) + f.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) + } +} diff --git a/service/internal/auth/jwt_test.go b/internal/auth/jwt_test.go similarity index 100% rename from service/internal/auth/jwt_test.go rename to internal/auth/jwt_test.go diff --git a/service/internal/middlewares/auth.go b/service/internal/middlewares/auth.go deleted file mode 100644 index bc43cb82..00000000 --- a/service/internal/middlewares/auth.go +++ /dev/null @@ -1,39 +0,0 @@ -package middlewares - -import ( - "context" - "net/http" - - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" -) - -type contextKey string - -var emailKey = contextKey("rmx-email") - -func Authenticate(publicKey jwk.Key) func(f http.Handler) http.Handler { - return func(f http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - // token, err := jwt.ParseRequest(r, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey(cookieName), jwt.WithKey(jwa.RS256, publicKey), jwt.WithValidate(true)) - token, err := jwt.ParseRequest(r, jwt.WithKey(jwa.ES256, publicKey)) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - email, ok := token.PrivateClaims()["email"].(string) - if !ok { - w.WriteHeader(http.StatusUnauthorized) - return - } - - // NOTE convert email from `string` type to `internal.Email` ? - r = r.WithContext(context.WithValue(r.Context(), emailKey, email)) - f.ServeHTTP(w, r) - } - - return http.HandlerFunc(fn) - } -} From e4e5e5a1932fa5bee9bd9e83f33289f0cebfb360 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Thu, 6 Oct 2022 12:38:01 +0330 Subject: [PATCH 126/246] fix refresh token flow (not tested) --- service/auth/routes.go | 15 ++++++++------- service/auth/service.go | 7 ++----- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/service/auth/routes.go b/service/auth/routes.go index ec311a74..37d76b93 100644 --- a/service/auth/routes.go +++ b/service/auth/routes.go @@ -6,6 +6,7 @@ import ( "github.com/lestrrat-go/jwx/v2/jwk" "github.com/rog-golang-buddies/rmx/internal/dto" + "github.com/rog-golang-buddies/rmx/internal/suid" "github.com/rog-golang-buddies/rmx/service/internal/auth" ) @@ -77,8 +78,11 @@ func (s *Service) handleSignIn(key jwk.Key) http.HandlerFunc { return } + // new Client ID since it's a new login request for a new connection + cid := suid.NewSUID() + // Generate new JWT tokens - its, ats, rts, err := s.signedTokens(key, string(ui.Email), ui.ID.String()) + its, ats, rts, err := s.signedTokens(key, string(ui.Email), cid.String()) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) return @@ -151,13 +155,10 @@ func (s *Service) handleRefreshToken(key jwk.Key) http.HandlerFunc { return } - ui, err := s.ur.LookupEmail(email) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } + // use the same Client ID since it's a refresh token request + cid := tc.Subject() - _, ats, rts, err := s.signedTokens(key, email, ui.ID.String()) + _, ats, rts, err := s.signedTokens(key, email, cid) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) return diff --git a/service/auth/service.go b/service/auth/service.go index 5716c0b6..c6555838 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -82,12 +82,10 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, return suid.ParseString(chi.URLParam(r, "uuid")) } -func (s *Service) signedTokens(key jwk.Key, email, uuid string) (its, ats, rts []byte, err error) { - // new client ID for tracking user connections - cid := suid.NewSUID() +func (s *Service) signedTokens(key jwk.Key, email, cid string) (its, ats, rts []byte, err error) { opt := auth.TokenOption{ Issuer: "github.com/rog-golang-buddies/rmx", - Subject: cid.String(), + Subject: cid, Expiration: time.Hour * 10, Claims: []fp.Tuple{{"email", email}}, } @@ -96,7 +94,6 @@ func (s *Service) signedTokens(key jwk.Key, email, uuid string) (its, ats, rts [ return nil, nil, nil, err } - opt.Subject = uuid opt.Expiration = auth.AccessTokenExpiry if ats, err = auth.SignToken(&key, &opt); err != nil { return nil, nil, nil, err From 896354c1e202ea77d4b3b6faf0b9c24204e0261b Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 10:35:35 +0100 Subject: [PATCH 127/246] create new service test --- service/auth/v2/service.go | 29 +++++++++++++++++++++- service/auth/v2/service_test.go | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 service/auth/v2/service_test.go diff --git a/service/auth/v2/service.go b/service/auth/v2/service.go index ee08ca70..ecf17379 100644 --- a/service/auth/v2/service.go +++ b/service/auth/v2/service.go @@ -21,7 +21,34 @@ var ( ErrSessionExists = errors.New("user: session already exists") ) -func (s *Service) routes() {} +/* +Register a new user + + [ ] POST /auth/register + +Create a cookie + + [ ] POST /auth/login + +Delete a cookie + + [ ] DELETE /auth/logout + +Refresh token + + [ ] GET /auth/refresh +*/ +func (s *Service) routes() { + s.m.Route("/api/v2/auth", func(r chi.Router) { + r.Post("/signup", s.handleSignUp()) + }) +} + +func (s *Service) handleSignUp() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + s.respond(w, r, nil, http.StatusNotImplemented) + } +} type Service struct { ctx context.Context diff --git a/service/auth/v2/service_test.go b/service/auth/v2/service_test.go new file mode 100644 index 00000000..4516e69b --- /dev/null +++ b/service/auth/v2/service_test.go @@ -0,0 +1,43 @@ +package auth + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/rog-golang-buddies/rmx/internal/dto" + "github.com/rog-golang-buddies/rmx/internal/is" +) + +const applicationJson = "application/json" + +var s http.Handler + +func init() { + var r dto.WUserRepo + + s = NewService(context.Background(), chi.NewMux(), r) +} + +func TestService(t *testing.T) { + t.Parallel() + is := is.New(t) + + srv := httptest.NewServer(s) + t.Cleanup(func() { srv.Close() }) + + t.Run("register a enw user", func(t *testing.T) { + payload := ` + { + "email":"fizz@gmail.com", + "username":"fizz_user", + "password:"fizz_$PW_10" + }` + + res, _ := srv.Client().Post(srv.URL+"/", applicationJson, strings.NewReader(payload)) + is.Equal(res.StatusCode, http.StatusCreated) + }) +} From 51419ede9ec1f8a9a48477f42700b52fba8f6e00 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 11:32:01 +0100 Subject: [PATCH 128/246] created password and password hash types --- internal/internal.go | 106 ++++++++++++++++++++++++++++++++++++++ internal/internal_test.go | 71 +++++++------------------ 2 files changed, 124 insertions(+), 53 deletions(-) diff --git a/internal/internal.go b/internal/internal.go index 562c2338..562169e5 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -1,7 +1,29 @@ package internal import ( + "context" + "errors" + "net/mail" "strings" + + "github.com/rog-golang-buddies/rmx/internal/suid" + gpv "github.com/wagslane/go-password-validator" + "golang.org/x/crypto/bcrypt" +) + +var ( + ErrInvalidEmail = errors.New("invalid email") + ErrNotImplemented = errors.New("not implemented") + ErrInvalidType = errors.New("invalid type") + ErrAlreadyExists = errors.New("already exists") + ErrNotFound = errors.New("not found") + ErrContextValue = errors.New("failed to retrieve value from context") +) + +type ContextKey string + +const ( + EmailKey = ContextKey("rmx-email") ) type MsgTyp int @@ -72,3 +94,87 @@ func (t *MsgTyp) MarshalJSON() ([]byte, error) { sb.WriteRune('"') return []byte(sb.String()), nil } + +type UserRepo interface { + RUserRepo + WUserRepo +} + +type RUserRepo interface { + // Lookup(uid *suid.UUID) (User, error) + // LookupEmail(email string) (User, error) + // ListAll() ([]User, error) +} + +type WUserRepo interface { + Insert(ctx context.Context, u *User) error + // Remove(uid *suid.UUID) error +} + +type User struct { + ID suid.UUID `json:"id"` + Username string `json:"username"` + Email Email `json:"email"` + Password PasswordHash `json:"-"` +} + +// Custom email type required +type Email string + +func (e *Email) String() string { return string(*e) } + +func (e *Email) IsValid() bool { return e.Valid() == nil } + +func (e *Email) Valid() error { + _, err := mail.ParseAddress(e.String()) + if err != nil { + return err + } + return nil +} + +func (e *Email) UnmarshalJSON(b []byte) error { + *e = Email(b[1 : len(b)-1]) + return e.Valid() +} + +// during production, this value needs to be > 40 +const minEntropy float64 = 10.0 + +// Custom password type required +type Password string + +func (p *Password) String() string { return string(*p) } + +func (p *Password) IsValid() bool { return p.Valid() == nil } + +func (p *Password) Valid() error { + return gpv.Validate(p.String(), minEntropy) +} + +func (p *Password) UnmarshalJSON(b []byte) error { + *p = Password(b[1 : len(b)-1]) + return p.Valid() +} + +func (p *Password) MarshalJSON() (b []byte, err error) { + var sb strings.Builder + sb.WriteRune('"') + sb.WriteString(string(*p)) + sb.WriteRune('"') + return []byte(sb.String()), nil +} + +func (p *Password) Hash() (PasswordHash, error) { return newPasswordHash(*p) } + +type PasswordHash []byte + +func (h *PasswordHash) String() string { return string(*h) } + +func newPasswordHash(pw Password) (PasswordHash, error) { + return bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) +} + +func (h *PasswordHash) Compare(cmp string) error { + return bcrypt.CompareHashAndPassword(*h, []byte(cmp)) +} diff --git a/internal/internal_test.go b/internal/internal_test.go index eb34c565..7d10e370 100644 --- a/internal/internal_test.go +++ b/internal/internal_test.go @@ -1,64 +1,29 @@ package internal -/* -func TestPasswordType(t *testing.T) { - t.Parallel() - - data := `"thispasswordiscomplex"` - - var pw dto.Password - err := json.NewDecoder(strings.NewReader(data)).Decode(&pw) - if err != nil { - t.Fatal(err) - } - - if exp := data[1 : len(data)-1]; pw != dto.Password(exp) { - t.Errorf(`expected %s got %s`, exp, pw) - } +import ( + "encoding/json" + "strings" + "testing" - hash, err := pw.Hash() - if err != nil { - t.Fatal(err) - } + "github.com/rog-golang-buddies/rmx/internal/is" +) - err = hash.Compare(pw) - if err != nil { - t.Fatal(err) - } - - err = hash.Compare("1234") - if err == nil { - t.Fatal("expected an error") - } -} - -func TestEmailType(t *testing.T) { +func TestPassword(t *testing.T) { t.Parallel() - data := `"anon@gmail.com"` - - var e dto.Email - err := json.NewDecoder(strings.NewReader(data)).Decode(&e) - if err != nil { - t.Fatal(err) - } + is := is.New(t) - var sb strings.Builder - err = json.NewEncoder(&sb).Encode(e) - if err != nil { - t.Fatal(err) - } + t.Run("(en/de)coding password type", func(t *testing.T) { + payload := `"thispasswordiscomplex"` - if s := strings.TrimSpace(sb.String()); s[1:len(s)-1] != string(e) { - t.Fatalf("expected %s got %s", e, s) - } + var pws Password + err := json.NewDecoder(strings.NewReader(payload)).Decode(&pws) + is.NoErr(err) - // should return an error - data = `"anon@.gmail.com"` + pw, err := pws.Hash() + is.NoErr(err) // hash password - err = json.NewDecoder(strings.NewReader(data)).Decode(&e) - if err == nil { - t.Errorf("expected %v", dto.ErrInvalidEmail) - } + err = pw.Compare(payload[1 : len(payload)-1]) + is.NoErr(err) // valid password + }) } -*/ From 1708880c3273d4dc23e25a331f9df0d18471c892 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 11:37:57 +0100 Subject: [PATCH 129/246] cleaned up marshalling --- internal/internal.go | 8 +++----- internal/internal_test.go | 24 ++++++++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/internal/internal.go b/internal/internal.go index 562169e5..4b2d6fa0 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -111,6 +111,7 @@ type WUserRepo interface { // Remove(uid *suid.UUID) error } +// Custom user type required type User struct { ID suid.UUID `json:"id"` Username string `json:"username"` @@ -127,10 +128,7 @@ func (e *Email) IsValid() bool { return e.Valid() == nil } func (e *Email) Valid() error { _, err := mail.ParseAddress(e.String()) - if err != nil { - return err - } - return nil + return err } func (e *Email) UnmarshalJSON(b []byte) error { @@ -160,7 +158,7 @@ func (p *Password) UnmarshalJSON(b []byte) error { func (p *Password) MarshalJSON() (b []byte, err error) { var sb strings.Builder sb.WriteRune('"') - sb.WriteString(string(*p)) + sb.WriteString(p.String()) sb.WriteRune('"') return []byte(sb.String()), nil } diff --git a/internal/internal_test.go b/internal/internal_test.go index 7d10e370..826a76b7 100644 --- a/internal/internal_test.go +++ b/internal/internal_test.go @@ -8,22 +8,30 @@ import ( "github.com/rog-golang-buddies/rmx/internal/is" ) -func TestPassword(t *testing.T) { +func TestCustomTypes(t *testing.T) { t.Parallel() is := is.New(t) - t.Run("(en/de)coding password type", func(t *testing.T) { - payload := `"thispasswordiscomplex"` + t.Run(`using "encoding/json" package with password`, func(t *testing.T) { + payload := `"this_password_is_complex"` - var pws Password - err := json.NewDecoder(strings.NewReader(payload)).Decode(&pws) - is.NoErr(err) + var p Password + err := json.NewDecoder(strings.NewReader(payload)).Decode(&p) + is.NoErr(err) // parse password - pw, err := pws.Hash() + h, err := p.Hash() is.NoErr(err) // hash password - err = pw.Compare(payload[1 : len(payload)-1]) + err = h.Compare(payload[1 : len(payload)-1]) is.NoErr(err) // valid password }) + + t.Run(`using "encoding/json" package with email`, func(t *testing.T) { + payload := `"fizz@mail.com"` + + var e Email + err := json.NewDecoder(strings.NewReader(payload)).Decode(&e) + is.NoErr(err) // parse email + }) } From 52d26ae37ecf72c90305b7f288f8f888801b045a Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 11:48:34 +0100 Subject: [PATCH 130/246] signup route --- service/auth/v2/service.go | 56 ++++++++++++++++++++++++++++++--- service/auth/v2/service_test.go | 9 +++--- store/db/v2/db.go | 16 ++++++++++ store/db/v2/user/user.go | 44 ++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 store/db/v2/db.go create mode 100644 store/db/v2/user/user.go diff --git a/service/auth/v2/service.go b/service/auth/v2/service.go index ecf17379..9e292fd8 100644 --- a/service/auth/v2/service.go +++ b/service/auth/v2/service.go @@ -10,7 +10,7 @@ import ( h "github.com/hyphengolang/prelude/http" - "github.com/rog-golang-buddies/rmx/internal/dto" + "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/suid" // big no-no ) @@ -40,13 +40,28 @@ Refresh token */ func (s *Service) routes() { s.m.Route("/api/v2/auth", func(r chi.Router) { + // tokens + }) + + s.m.Route("/api/v2/account", func(r chi.Router) { r.Post("/signup", s.handleSignUp()) }) } func (s *Service) handleSignUp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - s.respond(w, r, nil, http.StatusNotImplemented) + var u internal.User + if err := s.newUser(w, r, &u); err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + if err := s.r.Insert(r.Context(), &u); err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + s.created(w, r, u.ID.ShortUUID().String()) } } @@ -54,6 +69,7 @@ type Service struct { ctx context.Context m chi.Router + r internal.WUserRepo log func(...any) logf func(string, ...any) @@ -64,12 +80,40 @@ type Service struct { setCookie func(http.ResponseWriter, *http.Cookie) } +func (s *Service) newUser(w http.ResponseWriter, r *http.Request, u *internal.User) (err error) { + var dto User + if err = s.decode(w, r, &dto); err != nil { + return + } + + var h internal.PasswordHash + h, err = dto.Password.Hash() + if err != nil { + return + } + + *u = internal.User{ + ID: suid.NewUUID(), + Username: dto.Username, + Email: dto.Email, + Password: h, + } + + return nil +} + +func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { + return suid.ParseString(chi.URLParam(r, "uuid")) +} + func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func NewService(ctx context.Context, m chi.Router, r dto.UserRepo) *Service { +func NewService(ctx context.Context, m chi.Router, r internal.WUserRepo) *Service { s := &Service{ ctx, + m, + r, log.Println, log.Printf, @@ -84,6 +128,8 @@ func NewService(ctx context.Context, m chi.Router, r dto.UserRepo) *Service { return s } -func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { - return suid.ParseString(chi.URLParam(r, "uuid")) +type User struct { + Email internal.Email `json:"email"` + Username string `json:"username"` + Password internal.Password `json:"password"` } diff --git a/service/auth/v2/service_test.go b/service/auth/v2/service_test.go index 4516e69b..58d9737b 100644 --- a/service/auth/v2/service_test.go +++ b/service/auth/v2/service_test.go @@ -8,8 +8,8 @@ import ( "testing" "github.com/go-chi/chi/v5" - "github.com/rog-golang-buddies/rmx/internal/dto" "github.com/rog-golang-buddies/rmx/internal/is" + "github.com/rog-golang-buddies/rmx/store/db/v2/user" ) const applicationJson = "application/json" @@ -17,9 +17,8 @@ const applicationJson = "application/json" var s http.Handler func init() { - var r dto.WUserRepo - s = NewService(context.Background(), chi.NewMux(), r) + s = NewService(context.Background(), chi.NewMux(), user.MapRepo) } func TestService(t *testing.T) { @@ -34,10 +33,10 @@ func TestService(t *testing.T) { { "email":"fizz@gmail.com", "username":"fizz_user", - "password:"fizz_$PW_10" + "password":"fizz_$PW_10" }` - res, _ := srv.Client().Post(srv.URL+"/", applicationJson, strings.NewReader(payload)) + res, _ := srv.Client().Post(srv.URL+"/api/v2/account/signup", applicationJson, strings.NewReader(payload)) is.Equal(res.StatusCode, http.StatusCreated) }) } diff --git a/store/db/v2/db.go b/store/db/v2/db.go new file mode 100644 index 00000000..a6836672 --- /dev/null +++ b/store/db/v2/db.go @@ -0,0 +1,16 @@ +package user + +import ( + "context" + + "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/dto" +) + +var MapRepo = &repo{} + +type repo struct{} + +func (r repo) Insert(ctx context.Context, u *internal.User) error { + return dto.ErrNotImplemented +} diff --git a/store/db/v2/user/user.go b/store/db/v2/user/user.go new file mode 100644 index 00000000..64c78ca1 --- /dev/null +++ b/store/db/v2/user/user.go @@ -0,0 +1,44 @@ +package user + +import ( + "context" + "log" + "sync" + "time" + + "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/suid" +) + +type User struct { + ID suid.UUID + Username string + Email string + Password internal.PasswordHash + CreatedAt time.Time +} + +var MapRepo = &repo{miu: make(map[suid.UUID]*User), mei: make(map[string]*User), log: log.Println, logf: log.Printf} + +type repo struct { + mu sync.Mutex + miu map[suid.UUID]*User + mei map[string]*User + + log func(v ...any) + logf func(format string, v ...any) +} + +func (r *repo) Insert(ctx context.Context, iu *internal.User) error { + r.mu.Lock() + defer r.mu.Unlock() + + if _, found := r.mei[iu.Email.String()]; found { + return internal.ErrAlreadyExists + } + + u := &User{iu.ID, iu.Username, iu.Email.String(), iu.Password, time.Now()} + r.mei[iu.Email.String()], r.miu[iu.ID] = u, u + + return nil +} From adea26b81e9a24f4354dea411e5bc0ca16cdffdc Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 13:06:56 +0100 Subject: [PATCH 131/246] signToken function tested --- internal/auth/auth.go | 71 ++++++++++++++++--------- internal/auth/auth_test.go | 66 +++++++++++++++++++++++ internal/auth/jwt_test.go | 105 ------------------------------------- 3 files changed, 112 insertions(+), 130 deletions(-) create mode 100644 internal/auth/auth_test.go delete mode 100644 internal/auth/jwt_test.go diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 7a688c7b..9fce0236 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/rsa" "net/http" "time" @@ -13,6 +14,7 @@ import ( "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/pkg/errors" + "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/fp" ) @@ -108,43 +110,56 @@ func (c *Client) ValidateClientID(ctx context.Context, cid string) error { return ErrRTValidate } -/* -// would like to find an alternative to using `os` package -func LoadPEM(path string) (private, public jwk.Key, err error) { - buf, err := os.ReadFile(path) - if err != nil { - return - } +// Easier to pass an array that two variables with context +type Pair [2]jwk.Key - return GenerateKeys(string(buf)) -} -*/ +func (p *Pair) Private() jwk.Key { return p[0] } +func (p *Pair) Public() jwk.Key { return p[1] } -func GenerateKeys() (jwk.Key, jwk.Key, error) { - private, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) +func NewPairES256() Pair { + rawPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - return nil, nil, err + panic(err) } - key, err := jwk.FromRaw(private) + key, err := jwk.FromRaw(rawPriv) if err != nil { - return nil, nil, err + panic(err) } _, ok := key.(jwk.ECDSAPrivateKey) if !ok { - return nil, nil, ErrGenerateKey + panic(ErrGenerateKey) } pub, err := key.PublicKey() if err != nil { - return nil, nil, err + panic(err) } - return key, pub, nil + return Pair{key, pub} } -func SignToken(key *jwk.Key, opt *TokenOption) ([]byte, error) { +func NewPairRS256() Pair { + rawPrv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + + jwkPrv, err := jwk.FromRaw(rawPrv) + if err != nil { + panic(err) + } + + jwkPub, err := jwkPrv.PublicKey() + if err != nil { + panic(err) + } + + return Pair{jwkPrv, jwkPub} +} + +func SignToken(key jwk.Key, opt *TokenOption) ([]byte, error) { var t time.Time if opt.IssuedAt.IsZero() { t = time.Now().UTC() @@ -174,7 +189,14 @@ func SignToken(key *jwk.Key, opt *TokenOption) ([]byte, error) { } } - return jwt.Sign(token, jwt.WithKey(jwa.ES256, key)) + var algo jwa.SignatureAlgorithm + if opt.Algo == "" { + algo = jwa.RS256 + } else { + algo = opt.Algo + } + + return jwt.Sign(token, jwt.WithKey(algo, key)) } func ParseRefreshTokenClaims(token string) (jwt.Token, error) { return jwt.Parse([]byte(token)) } @@ -197,6 +219,7 @@ type TokenOption struct { Claims []fp.Tuple IssuedAt time.Time Expiration time.Duration + Algo jwa.SignatureAlgorithm } type authCtxKey string @@ -208,10 +231,7 @@ const ( EmailKey = authCtxKey("rmx-email") ) -type contextKey string - -var emailKey = contextKey("rmx-email") - +// This is authentication middleware which func Authenticate(publicKey jwk.Key) func(f http.Handler) http.Handler { return func(f http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { @@ -224,12 +244,13 @@ func Authenticate(publicKey jwk.Key) func(f http.Handler) http.Handler { email, ok := token.PrivateClaims()["email"].(string) if !ok { + // NOTE unsure if we need to write anything more to the body w.WriteHeader(http.StatusUnauthorized) return } // NOTE convert email from `string` type to `internal.Email` ? - r = r.WithContext(context.WithValue(r.Context(), emailKey, email)) + r = r.WithContext(context.WithValue(r.Context(), internal.EmailKey, email)) f.ServeHTTP(w, r) } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go new file mode 100644 index 00000000..239bf4db --- /dev/null +++ b/internal/auth/auth_test.go @@ -0,0 +1,66 @@ +package auth + +import ( + "testing" + "time" + + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/fp" + "github.com/rog-golang-buddies/rmx/internal/is" + "github.com/rog-golang-buddies/rmx/internal/suid" +) + +var rsaPrivateKey = `-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAML5MHFgqUlZcENS +hHZ83yXfoUpqaMfp5/UdgMIJ0S5DW5QEON6reAsDu6zP0BEVZhg65pEYWEraBrGK +Vcbx7dsVqK4Z0GMm0YRAvB+1K+pYlXwld90mwG1TqOKDPQXqC0Z/jZi6DSsAhfJU +WN0rkInZRtoVeRzbbh+nLN8nd14fAgMBAAECgYEAor+A2VL3XBvFIt0RZxpq5mFa +cBSMrDsqfSeIX+/z5SsimVZA5lW5GXCfSuwY4Pm8xAL+jSUGJk0CA1bWrP8rLByS +cQAy1q0odaAiWIG5zFUEQBg5Q5b3+jXmh2zwtO7yhPuXn1/vBGg+FvyR57gV/3F+ +TuBfR6Bc3VWKuj7Gm5kCQQDuRgm8HTDbX7IQ0EFAVKB73Pj4Gx5u2NieD9U8+qXx +JsAdn1vRvQ3mNJDR5OcTr4rPkpLLCtzjA2iTDXp4yqmrAkEA0Xp91LCpImKAOtM3 +4SGXdzKi9+7fWmcTtfkz996y9A1C9l27Cj92P7OFdwMB4Z/ZMizJd0eXYhXr4IxH +wBoxXQJAUBOXp/HDfqZdiIsEsuL+AEKWJYOvqZ8UxaIajuDJrg7Q1+O7jvRTXH9k +ADZGdnYzV2kyDiy7aUu29Fy+QSQS+wJAJyEsdBhz35pqvZJK8+DkfD2XN50FV8u9 +YNamIH0XDIOVqJOlpqpoGkocejizl0PWvIqlL4TOAGJ75zwNAxNheQJABEA2/hfF +GMJsOrnD74rGP/Lfpg882AmeUoT5eH766sSobFfUYJZvyAmnQoK2Lzg2hrKwXXix +JvEGfrhihVLb7g== +-----END PRIVATE KEY----- +` + +func TestToken(t *testing.T) { + t.Parallel() + is := is.New(t) + + t.Run(`generate a token and sign`, func(t *testing.T) { + key := NewPairES256() + + u := internal.User{ + ID: suid.NewUUID(), + Username: "fizz_user", + Email: "fizz@mail.com", + Password: internal.Password("492045rf-vf").MustHash(), + } + + opt := TokenOption{ + Issuer: "github.com/rog-golang-buddies/rmx", + Subject: suid.NewUUID().String(), + Expiration: time.Hour * 10, + Claims: []fp.Tuple{{"email", u.Email.String()}}, + Algo: jwa.ES256, + } + + _, err := SignToken(key.Private(), &opt) + is.NoErr(err) // sign id token + + opt.Subject = u.ID.String() + opt.Expiration = AccessTokenExpiry + _, err = SignToken(key.Private(), &opt) + is.NoErr(err) // access token + + opt.Expiration = RefreshTokenExpiry + _, err = SignToken(key.Private(), &opt) + is.NoErr(err) // refresh token + }) +} diff --git a/internal/auth/jwt_test.go b/internal/auth/jwt_test.go deleted file mode 100644 index 985ad78d..00000000 --- a/internal/auth/jwt_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package auth - -import ( - "fmt" - "net/http" - "os" - "testing" - - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" -) - -var rsaPrivateKey = `-----BEGIN PRIVATE KEY----- -MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAML5MHFgqUlZcENS -hHZ83yXfoUpqaMfp5/UdgMIJ0S5DW5QEON6reAsDu6zP0BEVZhg65pEYWEraBrGK -Vcbx7dsVqK4Z0GMm0YRAvB+1K+pYlXwld90mwG1TqOKDPQXqC0Z/jZi6DSsAhfJU -WN0rkInZRtoVeRzbbh+nLN8nd14fAgMBAAECgYEAor+A2VL3XBvFIt0RZxpq5mFa -cBSMrDsqfSeIX+/z5SsimVZA5lW5GXCfSuwY4Pm8xAL+jSUGJk0CA1bWrP8rLByS -cQAy1q0odaAiWIG5zFUEQBg5Q5b3+jXmh2zwtO7yhPuXn1/vBGg+FvyR57gV/3F+ -TuBfR6Bc3VWKuj7Gm5kCQQDuRgm8HTDbX7IQ0EFAVKB73Pj4Gx5u2NieD9U8+qXx -JsAdn1vRvQ3mNJDR5OcTr4rPkpLLCtzjA2iTDXp4yqmrAkEA0Xp91LCpImKAOtM3 -4SGXdzKi9+7fWmcTtfkz996y9A1C9l27Cj92P7OFdwMB4Z/ZMizJd0eXYhXr4IxH -wBoxXQJAUBOXp/HDfqZdiIsEsuL+AEKWJYOvqZ8UxaIajuDJrg7Q1+O7jvRTXH9k -ADZGdnYzV2kyDiy7aUu29Fy+QSQS+wJAJyEsdBhz35pqvZJK8+DkfD2XN50FV8u9 -YNamIH0XDIOVqJOlpqpoGkocejizl0PWvIqlL4TOAGJ75zwNAxNheQJABEA2/hfF -GMJsOrnD74rGP/Lfpg882AmeUoT5eH766sSobFfUYJZvyAmnQoK2Lzg2hrKwXXix -JvEGfrhihVLb7g== ------END PRIVATE KEY----- -` - -func TestJWT(t *testing.T) { - // -- Parse, Serialize JSON Web Key -- - jwkPrivate, err := jwk.ParseKey([]byte(rsaPrivateKey), jwk.WithPEM(true)) - if err != nil { - t.Fatalf("failed to parse JWK: %s\n", err) - } - - jwtPublic, err := jwk.PublicKeyOf(jwkPrivate) - if err != nil { - t.Fatalf("failed to get public key: %s\n", err) - } - // -- Parse, Serialize -- - - // -- Working with JSON Web Tokens -- - // build (server boot?) - token, err := jwt.NewBuilder().Build() - if err != nil { - t.Fatalf("failed to get public key: %s\n", err) - } - - // sign (server boot?) - signed, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, jwkPrivate)) - if err != nil { - t.Fatalf("failed to sign token: %s\n", err) - return - } - - // verify (every endpoint hit) - // verified, err := jwt.Parse(signed, jwt.WithKey(jwa.RS256, jwtPublic)) - // if err != nil { - // fmt.Printf("failed to verify JWS: %s\n", err) - // return - // } - // _ = verified - - req, err := http.NewRequest(http.MethodGet, `http://localhost:8080`, nil) - req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, signed)) - if err != nil { - t.Fatalf("failed to create HTTP request: %s\n", err) - return - } - - verifiedToken, err := jwt.ParseRequest(req, jwt.WithKey(jwa.RS256, jwtPublic)) - if err != nil { - t.Fatalf("failed to verify token from HTTP request: %s\n", err) - return - } - - _ = verifiedToken - // -- Working with htt.Request -- -} - -func TestCerts(t *testing.T) { - t.Parallel() - - // c := `../../certs/cert.pem` - k := `../../certs/key.pem` - buf, err := os.ReadFile(k) - if err != nil { - t.Fatalf("failed to read file %s\n", err) - } - - raw, _, err := jwk.DecodePEM(buf) - if err != nil { - t.Fatalf("failed to decode PEM key: %s\n", err) - } - - private, err := jwk.FromRaw(raw) - if err != nil { - t.Fatalf("failed to create private key") - } - - _ = private -} From 91073913afe34e55690a9dc5ffe2fc0a0d5d8a0d Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 13:07:34 +0100 Subject: [PATCH 132/246] create token --- service/auth/v2/service_test.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/service/auth/v2/service_test.go b/service/auth/v2/service_test.go index 58d9737b..edd66bbf 100644 --- a/service/auth/v2/service_test.go +++ b/service/auth/v2/service_test.go @@ -28,7 +28,7 @@ func TestService(t *testing.T) { srv := httptest.NewServer(s) t.Cleanup(func() { srv.Close() }) - t.Run("register a enw user", func(t *testing.T) { + t.Run("register a new user", func(t *testing.T) { payload := ` { "email":"fizz@gmail.com", @@ -36,7 +36,18 @@ func TestService(t *testing.T) { "password":"fizz_$PW_10" }` - res, _ := srv.Client().Post(srv.URL+"/api/v2/account/signup", applicationJson, strings.NewReader(payload)) + res, _ := srv.Client().Post(srv.URL+"/api/v2/account/sign-up", applicationJson, strings.NewReader(payload)) is.Equal(res.StatusCode, http.StatusCreated) }) + + t.Run(`user login`, func(t *testing.T) { + payload := ` + { + "email":"fizz@gmail.com", + "password":"fizz_$PW_10" + }` + + res, _ := srv.Client().Post(srv.URL+"/api/v2/auth/sign-in", applicationJson, strings.NewReader(payload)) + is.Equal(res.StatusCode, http.StatusOK) + }) } From a0146f501efcf408f67976fe293cc996a5467c14 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 13:08:34 +0100 Subject: [PATCH 133/246] tokenOption inc. algo option --- service/auth/v2/service.go | 124 +++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 6 deletions(-) diff --git a/service/auth/v2/service.go b/service/auth/v2/service.go index 9e292fd8..0f7a1672 100644 --- a/service/auth/v2/service.go +++ b/service/auth/v2/service.go @@ -5,12 +5,17 @@ import ( "errors" "log" "net/http" + "time" "github.com/go-chi/chi/v5" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" h "github.com/hyphengolang/prelude/http" "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/auth" + "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/suid" // big no-no ) @@ -24,11 +29,11 @@ var ( /* Register a new user - [ ] POST /auth/register + [?] POST /auth/register Create a cookie - [ ] POST /auth/login + [?] POST /auth/login Delete a cookie @@ -39,15 +44,86 @@ Refresh token [ ] GET /auth/refresh */ func (s *Service) routes() { + key := auth.NewPairES256() + s.m.Route("/api/v2/auth", func(r chi.Router) { - // tokens + r.Post("/sign-in", s.handleSignIn(key.Private())) + r.Delete("/sign-out", s.handleSignOut()) }) s.m.Route("/api/v2/account", func(r chi.Router) { - r.Post("/signup", s.handleSignUp()) + r.Post("/sign-up", s.handleSignUp()) }) } +func (s *Service) handleSignIn(privateKey jwk.Key) http.HandlerFunc { + type tokens struct { + IDToken string `json:"idToken"` + AccessToken string `json:"accessToken"` + } + + return func(w http.ResponseWriter, r *http.Request) { + var dto User + if err := s.decode(w, r, &dto); err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + u, err := s.r.Select(r.Context(), dto.Email) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + if err := u.Password.Compare(dto.Password.String()); err != nil { + s.respond(w, r, err, http.StatusUnauthorized) + return + } + + its, ats, rts, err := s.signedTokens(privateKey, u.Email.String(), u.ID.String()) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + c := &http.Cookie{ + Path: "/", + Name: auth.RefreshTokenCookieName, + Value: string(rts), + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteLaxMode, + MaxAge: int(auth.RefreshTokenExpiry), + } + + tk := &tokens{ + IDToken: string(its), + AccessToken: string(ats), + } + + s.setCookie(w, c) + s.respond(w, r, tk, http.StatusOK) + } +} + +// Account Sign Out handler +// removes the Refresh Token by setting its MaxAge property to -1 +func (s *Service) handleSignOut() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + c := &http.Cookie{ + Path: "/", + Name: auth.RefreshTokenCookieName, + HttpOnly: true, + // Secure: r.TLS != nil, + // SameSite: http.SameSiteLaxMode, + MaxAge: -1, + } + + s.setCookie(w, c) + s.respond(w, r, http.StatusText(http.StatusOK), http.StatusOK) + } +} + func (s *Service) handleSignUp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var u internal.User @@ -69,7 +145,7 @@ type Service struct { ctx context.Context m chi.Router - r internal.WUserRepo + r internal.UserRepo log func(...any) logf func(string, ...any) @@ -106,9 +182,38 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, return suid.ParseString(chi.URLParam(r, "uuid")) } +func (s *Service) signedTokens(key jwk.Key, email, uuid string) (its, ats, rts []byte, err error) { + // new client ID for tracking user connections + cid := suid.NewSUID() + opt := auth.TokenOption{ + Issuer: "github.com/rog-golang-buddies/rmx", + Subject: cid.String(), + Expiration: time.Hour * 10, + Claims: []fp.Tuple{{"email", email}}, + Algo: jwa.ES256, + } + + if its, err = auth.SignToken(key, &opt); err != nil { + return nil, nil, nil, err + } + + opt.Subject = uuid + opt.Expiration = time.Minute * 5 + if ats, err = auth.SignToken(key, &opt); err != nil { + return nil, nil, nil, err + } + + opt.Expiration = time.Hour * 24 * 7 + if rts, err = auth.SignToken(key, &opt); err != nil { + return nil, nil, nil, err + } + + return its, ats, rts, nil +} + func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func NewService(ctx context.Context, m chi.Router, r internal.WUserRepo) *Service { +func NewService(ctx context.Context, m chi.Router, r internal.UserRepo) *Service { s := &Service{ ctx, @@ -128,6 +233,13 @@ func NewService(ctx context.Context, m chi.Router, r internal.WUserRepo) *Servic return s } +func (s *Service) Context() context.Context { + if s.ctx != nil { + return s.ctx + } + return context.Background() +} + type User struct { Email internal.Email `json:"email"` Username string `json:"username"` From 311baa38cba506c6cbe1ab80d44e775c02d03025 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 13:36:32 +0100 Subject: [PATCH 134/246] indentity and sign-out methods tested --- service/auth/v2/service.go | 20 +++++++++++++++++--- service/auth/v2/service_test.go | 26 ++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/service/auth/v2/service.go b/service/auth/v2/service.go index 0f7a1672..6d88320f 100644 --- a/service/auth/v2/service.go +++ b/service/auth/v2/service.go @@ -49,13 +49,29 @@ func (s *Service) routes() { s.m.Route("/api/v2/auth", func(r chi.Router) { r.Post("/sign-in", s.handleSignIn(key.Private())) r.Delete("/sign-out", s.handleSignOut()) + r.Post("/sign-up", s.handleSignUp()) }) s.m.Route("/api/v2/account", func(r chi.Router) { - r.Post("/sign-up", s.handleSignUp()) + auth := r.With(auth.Authenticate(jwa.ES256, key.Public())) + auth.Get("/me", s.handleIdentity()) }) } +func (s *Service) handleIdentity() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + email := r.Context().Value(internal.EmailKey).(internal.Email) + + u, err := s.r.Select(r.Context(), email) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + s.respond(w, r, u, http.StatusOK) + } +} + func (s *Service) handleSignIn(privateKey jwk.Key) http.HandlerFunc { type tokens struct { IDToken string `json:"idToken"` @@ -106,8 +122,6 @@ func (s *Service) handleSignIn(privateKey jwk.Key) http.HandlerFunc { } } -// Account Sign Out handler -// removes the Refresh Token by setting its MaxAge property to -1 func (s *Service) handleSignOut() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { c := &http.Cookie{ diff --git a/service/auth/v2/service_test.go b/service/auth/v2/service_test.go index edd66bbf..423f9b0a 100644 --- a/service/auth/v2/service_test.go +++ b/service/auth/v2/service_test.go @@ -2,6 +2,8 @@ package auth import ( "context" + "encoding/json" + "fmt" "net/http" "net/http/httptest" "strings" @@ -36,11 +38,11 @@ func TestService(t *testing.T) { "password":"fizz_$PW_10" }` - res, _ := srv.Client().Post(srv.URL+"/api/v2/account/sign-up", applicationJson, strings.NewReader(payload)) + res, _ := srv.Client().Post(srv.URL+"/api/v2/auth/sign-up", applicationJson, strings.NewReader(payload)) is.Equal(res.StatusCode, http.StatusCreated) }) - t.Run(`user login`, func(t *testing.T) { + t.Run("sign-in, access auth endpoint then sign-out", func(t *testing.T) { payload := ` { "email":"fizz@gmail.com", @@ -49,5 +51,25 @@ func TestService(t *testing.T) { res, _ := srv.Client().Post(srv.URL+"/api/v2/auth/sign-in", applicationJson, strings.NewReader(payload)) is.Equal(res.StatusCode, http.StatusOK) + + type body struct { + IDToken string `json:"idToken"` + AccessToken string `json:"accessToken"` + } + + var b body + err := json.NewDecoder(res.Body).Decode(&b) + res.Body.Close() + is.NoErr(err) // parsing json + + req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/v2/account/me", nil) + req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, b.AccessToken)) + res, _ = srv.Client().Do(req) + is.Equal(res.StatusCode, http.StatusOK) // authorized endpoint + + req, _ = http.NewRequest(http.MethodDelete, srv.URL+"/api/v2/auth/sign-out", nil) + req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, b.AccessToken)) + res, _ = srv.Client().Do(req) + is.Equal(res.StatusCode, http.StatusOK) // delete cookie }) } From 0eea8865206ff22ae03472e9e30b96319bc8a413 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 13:37:08 +0100 Subject: [PATCH 135/246] auth mw now includes algoTyp as an argument --- internal/auth/auth.go | 8 ++++---- internal/auth/auth_test.go | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 9fce0236..f1e16019 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -231,12 +231,12 @@ const ( EmailKey = authCtxKey("rmx-email") ) -// This is authentication middleware which -func Authenticate(publicKey jwk.Key) func(f http.Handler) http.Handler { +// authentication middleware +func Authenticate(algo jwa.SignatureAlgorithm, publicKey jwk.Key) func(f http.Handler) http.Handler { return func(f http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { // token, err := jwt.ParseRequest(r, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey(cookieName), jwt.WithKey(jwa.RS256, publicKey), jwt.WithValidate(true)) - token, err := jwt.ParseRequest(r, jwt.WithKey(jwa.ES256, publicKey)) + token, err := jwt.ParseRequest(r, jwt.WithKey(algo, publicKey)) if err != nil { w.WriteHeader(http.StatusUnauthorized) return @@ -250,7 +250,7 @@ func Authenticate(publicKey jwk.Key) func(f http.Handler) http.Handler { } // NOTE convert email from `string` type to `internal.Email` ? - r = r.WithContext(context.WithValue(r.Context(), internal.EmailKey, email)) + r = r.WithContext(context.WithValue(r.Context(), internal.EmailKey, internal.Email(email))) f.ServeHTTP(w, r) } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 239bf4db..b2241ceb 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -64,3 +64,12 @@ func TestToken(t *testing.T) { is.NoErr(err) // refresh token }) } + +func TestMiddleware(t *testing.T) { + t.Parallel() + is := is.New(t) + + t.Run("authorize access to protected endpoint", func(t *testing.T) { + // httptest.NewRequest() + }) +} From 36e165fe7de6f930c6de3e2f58da835382e2c4df Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 14:41:25 +0100 Subject: [PATCH 136/246] signedTokens helper takes in an id value --- service/auth/v2/service.go | 43 ++++++++++++++++++++++++--------- service/auth/v2/service_test.go | 1 - 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/service/auth/v2/service.go b/service/auth/v2/service.go index 6d88320f..4df307a2 100644 --- a/service/auth/v2/service.go +++ b/service/auth/v2/service.go @@ -29,15 +29,19 @@ var ( /* Register a new user - [?] POST /auth/register + [?] POST /auth/sign-up + +Get current account identity + + [?] GET /account/me Create a cookie - [?] POST /auth/login + [?] POST /auth/sign-in Delete a cookie - [ ] DELETE /auth/logout + [?] DELETE /auth/sign-out Refresh token @@ -50,6 +54,8 @@ func (s *Service) routes() { r.Post("/sign-in", s.handleSignIn(key.Private())) r.Delete("/sign-out", s.handleSignOut()) r.Post("/sign-up", s.handleSignUp()) + + r.Get("/refresh", s.handleRefresh(key.Private())) }) s.m.Route("/api/v2/account", func(r chi.Router) { @@ -58,6 +64,23 @@ func (s *Service) routes() { }) } +func (s *Service) handleRefresh(key jwk.Key) http.HandlerFunc { + type token struct { + AccessToken string `json:"accessToken"` + } + + return func(w http.ResponseWriter, r *http.Request) { + c, err := r.Cookie(auth.RefreshTokenCookieName) + if err != nil { + s.respond(w, r, err, http.StatusUnauthorized) + return + } + + s.setCookie(w, c) + s.respond(w, r, nil, http.StatusNotImplemented) + } +} + func (s *Service) handleIdentity() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { email := r.Context().Value(internal.EmailKey).(internal.Email) @@ -73,7 +96,7 @@ func (s *Service) handleIdentity() http.HandlerFunc { } func (s *Service) handleSignIn(privateKey jwk.Key) http.HandlerFunc { - type tokens struct { + type token struct { IDToken string `json:"idToken"` AccessToken string `json:"accessToken"` } @@ -96,7 +119,7 @@ func (s *Service) handleSignIn(privateKey jwk.Key) http.HandlerFunc { return } - its, ats, rts, err := s.signedTokens(privateKey, u.Email.String(), u.ID.String()) + its, ats, rts, err := s.signedTokens(privateKey, u.Email.String(), suid.NewUUID()) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) return @@ -112,7 +135,7 @@ func (s *Service) handleSignIn(privateKey jwk.Key) http.HandlerFunc { MaxAge: int(auth.RefreshTokenExpiry), } - tk := &tokens{ + tk := &token{ IDToken: string(its), AccessToken: string(ats), } @@ -196,12 +219,11 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, return suid.ParseString(chi.URLParam(r, "uuid")) } -func (s *Service) signedTokens(key jwk.Key, email, uuid string) (its, ats, rts []byte, err error) { - // new client ID for tracking user connections - cid := suid.NewSUID() +// TODO there is two cid's being used here, need clarification +func (s *Service) signedTokens(key jwk.Key, email string, uuid suid.UUID) (its, ats, rts []byte, err error) { opt := auth.TokenOption{ Issuer: "github.com/rog-golang-buddies/rmx", - Subject: cid.String(), + Subject: uuid.String(), // new client ID for tracking user connections Expiration: time.Hour * 10, Claims: []fp.Tuple{{"email", email}}, Algo: jwa.ES256, @@ -211,7 +233,6 @@ func (s *Service) signedTokens(key jwk.Key, email, uuid string) (its, ats, rts [ return nil, nil, nil, err } - opt.Subject = uuid opt.Expiration = time.Minute * 5 if ats, err = auth.SignToken(key, &opt); err != nil { return nil, nil, nil, err diff --git a/service/auth/v2/service_test.go b/service/auth/v2/service_test.go index 423f9b0a..186cb24f 100644 --- a/service/auth/v2/service_test.go +++ b/service/auth/v2/service_test.go @@ -19,7 +19,6 @@ const applicationJson = "application/json" var s http.Handler func init() { - s = NewService(context.Background(), chi.NewMux(), user.MapRepo) } From 4f99ad5b4ceda755b3114d7ff4910facc1e3d606 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 14:42:31 +0100 Subject: [PATCH 137/246] add in-mem repo --- store/db/v2/user/user.go | 63 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/store/db/v2/user/user.go b/store/db/v2/user/user.go index 64c78ca1..3cfe4e6a 100644 --- a/store/db/v2/user/user.go +++ b/store/db/v2/user/user.go @@ -42,3 +42,66 @@ func (r *repo) Insert(ctx context.Context, iu *internal.User) error { return nil } + +func (r *repo) Select(ctx context.Context, key any) (*internal.User, error) { + switch key := key.(type) { + case suid.UUID: + return r.selectUUID(key) + case internal.Email: + return r.selectEmail(key) + case string: + return r.selectUsername(key) + default: + return nil, internal.ErrInvalidType + } +} + +func (r *repo) selectUUID(uid suid.UUID) (*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() + + if u, ok := r.miu[uid]; ok { + return &internal.User{ + ID: u.ID, + Username: u.Username, + Email: internal.Email(u.Email), + Password: u.Password, + }, nil + } + + return nil, internal.ErrNotFound +} + +func (r *repo) selectUsername(username string) (*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() + + for _, u := range r.mei { + if u.Username == username { + return &internal.User{ + ID: u.ID, + Username: u.Username, + Email: internal.Email(u.Email), + Password: u.Password, + }, nil + } + } + + return nil, internal.ErrNotFound +} + +func (r *repo) selectEmail(email internal.Email) (*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() + + if u, ok := r.mei[email.String()]; ok { + return &internal.User{ + ID: u.ID, + Username: u.Username, + Email: internal.Email(u.Email), + Password: u.Password, + }, nil + } + + return nil, internal.ErrNotFound +} From d8e2cf68bde3016959314b5d37af21dc3b975df4 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 14:42:44 +0100 Subject: [PATCH 138/246] look-back at v1 --- internal/dto/dto.go | 11 ++++++----- internal/internal.go | 28 ++++++++++++++++++---------- service/auth/routes.go | 15 ++++++++------- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/internal/dto/dto.go b/internal/dto/dto.go index 1801c2c1..9846504d 100644 --- a/internal/dto/dto.go +++ b/internal/dto/dto.go @@ -1,6 +1,7 @@ package dto import ( + "context" "errors" "net/mail" @@ -41,14 +42,14 @@ type UserRepo interface { } type RUserRepo interface { - Lookup(uid *suid.UUID) (User, error) - LookupEmail(email string) (User, error) - ListAll() ([]User, error) + // Lookup(uid *suid.UUID) (User, error) + // LookupEmail(email string) (User, error) + // ListAll() ([]User, error) } type WUserRepo interface { - Add(u *User) error - Remove(uid *suid.UUID) error + Insert(ctx context.Context, u *User) error + // Remove(uid *suid.UUID) error } /* diff --git a/internal/internal.go b/internal/internal.go index 4b2d6fa0..a610bceb 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -104,6 +104,7 @@ type RUserRepo interface { // Lookup(uid *suid.UUID) (User, error) // LookupEmail(email string) (User, error) // ListAll() ([]User, error) + Select(ctx context.Context, key any) (*User, error) } type WUserRepo interface { @@ -137,16 +138,16 @@ func (e *Email) UnmarshalJSON(b []byte) error { } // during production, this value needs to be > 40 -const minEntropy float64 = 10.0 +const minEntropy float64 = 50.0 // Custom password type required type Password string -func (p *Password) String() string { return string(*p) } +func (p Password) String() string { return string(p) } -func (p *Password) IsValid() bool { return p.Valid() == nil } +func (p Password) IsValid() bool { return p.Valid() == nil } -func (p *Password) Valid() error { +func (p Password) Valid() error { return gpv.Validate(p.String(), minEntropy) } @@ -155,7 +156,7 @@ func (p *Password) UnmarshalJSON(b []byte) error { return p.Valid() } -func (p *Password) MarshalJSON() (b []byte, err error) { +func (p Password) MarshalJSON() (b []byte, err error) { var sb strings.Builder sb.WriteRune('"') sb.WriteString(p.String()) @@ -163,16 +164,23 @@ func (p *Password) MarshalJSON() (b []byte, err error) { return []byte(sb.String()), nil } -func (p *Password) Hash() (PasswordHash, error) { return newPasswordHash(*p) } +func (p Password) Hash() (PasswordHash, error) { + return bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost) +} + +func (p Password) MustHash() PasswordHash { + h, err := p.Hash() + if err != nil { + panic(err) + } + + return h +} type PasswordHash []byte func (h *PasswordHash) String() string { return string(*h) } -func newPasswordHash(pw Password) (PasswordHash, error) { - return bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) -} - func (h *PasswordHash) Compare(cmp string) error { return bcrypt.CompareHashAndPassword(*h, []byte(cmp)) } diff --git a/service/auth/routes.go b/service/auth/routes.go index ec311a74..37d76b93 100644 --- a/service/auth/routes.go +++ b/service/auth/routes.go @@ -6,6 +6,7 @@ import ( "github.com/lestrrat-go/jwx/v2/jwk" "github.com/rog-golang-buddies/rmx/internal/dto" + "github.com/rog-golang-buddies/rmx/internal/suid" "github.com/rog-golang-buddies/rmx/service/internal/auth" ) @@ -77,8 +78,11 @@ func (s *Service) handleSignIn(key jwk.Key) http.HandlerFunc { return } + // new Client ID since it's a new login request for a new connection + cid := suid.NewSUID() + // Generate new JWT tokens - its, ats, rts, err := s.signedTokens(key, string(ui.Email), ui.ID.String()) + its, ats, rts, err := s.signedTokens(key, string(ui.Email), cid.String()) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) return @@ -151,13 +155,10 @@ func (s *Service) handleRefreshToken(key jwk.Key) http.HandlerFunc { return } - ui, err := s.ur.LookupEmail(email) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } + // use the same Client ID since it's a refresh token request + cid := tc.Subject() - _, ats, rts, err := s.signedTokens(key, email, ui.ID.String()) + _, ats, rts, err := s.signedTokens(key, email, cid) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) return From 22086f028e4fe093f3770139980af721c96ca8f5 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 19:37:58 +0100 Subject: [PATCH 139/246] added testing for authentication --- internal/auth/auth.go | 106 +++++++++++++++++++++++++++++++++---- internal/auth/auth_test.go | 69 +++++++++++++++++++++++- 2 files changed, 162 insertions(+), 13 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index f1e16019..e4b5e572 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -225,18 +225,68 @@ type TokenOption struct { type authCtxKey string const ( - RefreshTokenCookieName = "RMX_REFRESH_TOKEN" - RefreshTokenExpiry = time.Hour * 24 * 7 - AccessTokenExpiry = time.Minute * 5 - EmailKey = authCtxKey("rmx-email") + // RefreshTokenCookieName = "RMX_REFRESH_TOKEN" + RefreshTokenExpiry = time.Hour * 24 * 7 + AccessTokenExpiry = time.Minute * 5 + EmailKey = authCtxKey("rmx-email") ) -// authentication middleware -func Authenticate(algo jwa.SignatureAlgorithm, publicKey jwk.Key) func(f http.Handler) http.Handler { - return func(f http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { +/* + */ +func Authentication(algo jwa.SignatureAlgorithm, key jwk.Key, cookieName ...string) func(http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + at := func(w http.ResponseWriter, r *http.Request) { + token, err := jwt.ParseRequest(r, jwt.WithKey(algo, key)) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + email, ok := token.PrivateClaims()["email"].(string) + if !ok { + // NOTE unsure if we need to write anything more to the body + w.WriteHeader(http.StatusUnauthorized) + return + } + + // NOTE convert email from `string` type to `internal.Email` ? + r = r.WithContext(context.WithValue(r.Context(), internal.EmailKey, internal.Email(email))) + h.ServeHTTP(w, r) + } + + rt := func(w http.ResponseWriter, r *http.Request) { + token, err := jwt.ParseRequest(r, jwt.WithKey(algo, key), jwt.WithHeaderKey(cookieName[0])) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + email, ok := token.PrivateClaims()["email"].(string) + if !ok { + // NOTE unsure if we need to write anything more to the body + w.WriteHeader(http.StatusUnauthorized) + return + } + + r = r.WithContext(context.WithValue(r.Context(), internal.EmailKey, internal.Email(email))) + r = r.WithContext(context.WithValue(r.Context(), internal.TokenKey, token)) + h.ServeHTTP(w, r) + } + + if len(cookieName) != 0 { + return http.HandlerFunc(rt) + } + + return http.HandlerFunc(at) + } +} + +// Authenticate against the accessToken +func Authenticate(algo jwa.SignatureAlgorithm, key jwk.Key) func(http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + f := func(w http.ResponseWriter, r *http.Request) { // token, err := jwt.ParseRequest(r, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey(cookieName), jwt.WithKey(jwa.RS256, publicKey), jwt.WithValidate(true)) - token, err := jwt.ParseRequest(r, jwt.WithKey(algo, publicKey)) + token, err := jwt.ParseRequest(r, jwt.WithKey(algo, key)) if err != nil { w.WriteHeader(http.StatusUnauthorized) return @@ -251,9 +301,43 @@ func Authenticate(algo jwa.SignatureAlgorithm, publicKey jwk.Key) func(f http.Ha // NOTE convert email from `string` type to `internal.Email` ? r = r.WithContext(context.WithValue(r.Context(), internal.EmailKey, internal.Email(email))) - f.ServeHTTP(w, r) + h.ServeHTTP(w, r) + } + + return http.HandlerFunc(f) + } +} + +// Authenticate against the refreshToken +func AuthenticateRefresh(algo jwa.SignatureAlgorithm, key jwk.Key, cookieName string) func(http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + var f http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + rc, err := r.Cookie(cookieName) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + token, err := jwt.Parse([]byte(rc.Value), jwt.WithKey(algo, key), jwt.WithValidate(true)) + + // token, err := jwt.ParseRequest(r, jwt.WithKey(algo, key), jwt.WithHeaderKey(cookieName)) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + email, ok := token.PrivateClaims()["email"].(string) + if !ok { + // NOTE unsure if we need to write anything more to the body + w.WriteHeader(http.StatusUnauthorized) + return + } + + r = r.WithContext(context.WithValue(r.Context(), internal.EmailKey, internal.Email(email))) + r = r.WithContext(context.WithValue(r.Context(), internal.TokenKey, token)) + h.ServeHTTP(w, r) } - return http.HandlerFunc(fn) + return http.HandlerFunc(f) } } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index b2241ceb..e0a6211a 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -1,6 +1,9 @@ package auth import ( + "fmt" + "net/http" + "net/http/httptest" "testing" "time" @@ -69,7 +72,69 @@ func TestMiddleware(t *testing.T) { t.Parallel() is := is.New(t) - t.Run("authorize access to protected endpoint", func(t *testing.T) { - // httptest.NewRequest() + t.Run("authenticate against Authorization header", func(t *testing.T) { + key := NewPairES256() + + e := internal.Email("foobar@gmail.com") + + opt := TokenOption{ + Issuer: "github.com/rog-golang-buddies/rmx", + Subject: suid.NewUUID().String(), + Expiration: time.Hour * 10, + Claims: []fp.Tuple{{"email", e.String()}}, + Algo: jwa.ES256, + } + + // ats + ats, err := SignToken(key.Private(), &opt) + is.NoErr(err) // signing access token + + h := Authenticate(opt.Algo, key.Public())(http.NotFoundHandler()) + + req, _ := http.NewRequest(http.MethodGet, "/", nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(ats))) + + res := httptest.NewRecorder() + + h.ServeHTTP(res, req) + is.Equal(res.Result().StatusCode, http.StatusNotFound) // return not found + }) + + t.Run("authenticate against Cookie header", func(t *testing.T) { + key := NewPairES256() + + e := internal.Email("foobar@gmail.com") + + opt := TokenOption{ + Issuer: "github.com/rog-golang-buddies/rmx", + Subject: suid.NewUUID().String(), + Expiration: time.Hour * 10, + Claims: []fp.Tuple{{"email", e.String()}}, + Algo: jwa.ES256, + } + + // rts + rts, err := SignToken(key.Private(), &opt) + is.NoErr(err) // signing refresh token + + cookieName := `__myCookie` + + h := AuthenticateRefresh(opt.Algo, key.Public(), cookieName)(http.NotFoundHandler()) + + req, _ := http.NewRequest(http.MethodGet, "/", nil) + req.AddCookie(&http.Cookie{ + Path: "/", + Name: cookieName, + Value: string(rts), + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + MaxAge: 24 * 7, + }) + + res := httptest.NewRecorder() + + h.ServeHTTP(res, req) + is.Equal(res.Result().StatusCode, http.StatusNotFound) // http page not found }) + } From 913a06860b1af76cbc07229217ff0fe25992d62b Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 19:38:24 +0100 Subject: [PATCH 140/246] auth redis client --- service/internal/auth/auth.go | 207 ++++++++++++++++++++++++++++++++++ store/db/v2/auth/auth.go | 109 ++++++++++++++++++ 2 files changed, 316 insertions(+) create mode 100644 service/internal/auth/auth.go create mode 100644 store/db/v2/auth/auth.go diff --git a/service/internal/auth/auth.go b/service/internal/auth/auth.go new file mode 100644 index 00000000..bccb9a2d --- /dev/null +++ b/service/internal/auth/auth.go @@ -0,0 +1,207 @@ +package auth + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "time" + + "github.com/go-redis/redis/v9" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/pkg/errors" + "github.com/rog-golang-buddies/rmx/internal/fp" +) + +type Client struct { + rtdb, cidb *redis.Client +} + +var ( + ErrNotImplemented = errors.New("not implemented") + ErrGenerateKey = errors.New("failed to generate new ecdsa key pair") + ErrSignTokens = errors.New("failed to generate signed tokens") + ErrRTValidate = errors.New("failed to validate refresh token") +) + +func NewRedis(addr, password string) *Client { + rtdb := redis.Options{Addr: addr, Password: password, DB: 0} + cidb := redis.Options{Addr: addr, Password: password, DB: 1} + + c := &Client{redis.NewClient(&rtdb), redis.NewClient(&cidb)} + return c +} + +const ( + defaultAddr = "localhost:6379" + defaultPassword = "" +) + +var DefaultClient = &Client{ + rtdb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 0}), + cidb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 1}), +} + +func (c *Client) ValidateRefreshToken(ctx context.Context, token string) error { + tc, err := ParseRefreshTokenClaims(token) + if err != nil { + return err + } + email, ok := tc.PrivateClaims()["email"].(string) + if !ok { + return ErrRTValidate + } + + cid := tc.Subject() + if err := c.ValidateClientID(ctx, cid); err != nil { + return err + } + + if _, err := c.rtdb.Get(ctx, token).Result(); err != nil { + switch err { + case redis.Nil: + return nil + default: + return err + } + } + + err = c.RevokeClientID(ctx, cid, email) + if err != nil { + return err + } + + return ErrRTValidate +} + +func (c *Client) RevokeClientID(ctx context.Context, cid, email string) error { + _, err := c.cidb.Set(ctx, cid, email, RefreshTokenExpiry).Result() + if err != nil { + return err + } + return nil +} + +func (c *Client) RevokeRefreshToken(ctx context.Context, token string) error { + _, err := c.rtdb.Set(ctx, token, nil, RefreshTokenExpiry).Result() + return err +} + +func (c *Client) ValidateClientID(ctx context.Context, cid string) error { + // check if a key with client id exists + // if the key exists it means that the client id is revoked and token should be denied + // we don't need the email value here + _, err := c.cidb.Get(ctx, cid).Result() + if err != nil { + switch err { + case redis.Nil: + return nil + default: + return ErrRTValidate + } + } + + return ErrRTValidate +} + +/* +// would like to find an alternative to using `os` package +func LoadPEM(path string) (private, public jwk.Key, err error) { + buf, err := os.ReadFile(path) + if err != nil { + return + } + + return GenerateKeys(string(buf)) +} +*/ + +func GenerateKeys() (jwk.Key, jwk.Key, error) { + private, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + key, err := jwk.FromRaw(private) + if err != nil { + return nil, nil, err + } + + _, ok := key.(jwk.ECDSAPrivateKey) + if !ok { + return nil, nil, ErrGenerateKey + } + + pub, err := key.PublicKey() + if err != nil { + return nil, nil, err + } + + return key, pub, nil +} + +func SignToken(key *jwk.Key, opt *TokenOption) ([]byte, error) { + var t time.Time + if opt.IssuedAt.IsZero() { + t = time.Now().UTC() + } else { + t = opt.IssuedAt + } + + token, err := jwt.NewBuilder(). + Issuer(opt.Issuer). + Audience(opt.Audience). + Subject(opt.Subject). + IssuedAt(t). + Expiration(t.Add(opt.Expiration)). + Build() + if err != nil { + return nil, ErrSignTokens + } + + for _, c := range opt.Claims { + if !c.HasValue() { + return nil, fp.ErrTuple + } + + err := token.Set(c[0], c[1]) + if err != nil { + return nil, ErrSignTokens + } + } + + return jwt.Sign(token, jwt.WithKey(jwa.ES256, key)) +} + +func ParseRefreshTokenClaims(token string) (jwt.Token, error) { return jwt.Parse([]byte(token)) } + +func ParseRefreshTokenWithValidate(key *jwk.Key, token string) (jwt.Token, error) { + payload, err := jwt.Parse([]byte(token), + jwt.WithKey(jwa.ES256, key), + jwt.WithValidate(true)) + if err != nil { + return nil, err + } + + return payload, nil +} + +type TokenOption struct { + Issuer string + Audience []string + Subject string + Claims []fp.Tuple + IssuedAt time.Time + Expiration time.Duration +} + +type authCtxKey string + +const ( + RefreshTokenCookieName = "RMX_REFRESH_TOKEN" + RefreshTokenExpiry = time.Hour * 24 * 7 + AccessTokenExpiry = time.Minute * 5 + EmailKey = authCtxKey("rmx-email") +) diff --git a/store/db/v2/auth/auth.go b/store/db/v2/auth/auth.go new file mode 100644 index 00000000..c6009758 --- /dev/null +++ b/store/db/v2/auth/auth.go @@ -0,0 +1,109 @@ +package auth + +import ( + "context" + "errors" + "time" + + "github.com/go-redis/redis/v9" + "github.com/lestrrat-go/jwx/v2/jwt" +) + +type Client struct { + rtdb, cidb *redis.Client +} + +var ( + ErrNotImplemented = errors.New("not implemented") + ErrGenerateKey = errors.New("failed to generate new ecdsa key pair") + ErrSignTokens = errors.New("failed to generate signed tokens") + ErrRTValidate = errors.New("failed to validate refresh token") +) + +func NewRedis(addr, password string) *Client { + rtdb := redis.Options{Addr: addr, Password: password, DB: 0} + cidb := redis.Options{Addr: addr, Password: password, DB: 1} + + c := &Client{redis.NewClient(&rtdb), redis.NewClient(&cidb)} + return c +} + +const ( + defaultAddr = "localhost:6379" + defaultPassword = "" +) + +var DefaultClient = &Client{ + rtdb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 0}), + cidb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 1}), +} + +func (c *Client) ValidateRefreshToken(ctx context.Context, token string) error { + tc, err := ParseRefreshTokenClaims(token) + if err != nil { + return err + } + + cid := tc.Subject() + email, ok := tc.PrivateClaims()["email"].(string) + if !ok { + return ErrRTValidate + } + + if err := c.ValidateClientID(ctx, cid); err != nil { + return err + } + + if _, err := c.rtdb.Get(ctx, token).Result(); err != nil { + switch err { + case redis.Nil: + return nil + default: + return err + } + } + + err = c.RevokeClientID(ctx, cid, email) + if err != nil { + return err + } + + return ErrRTValidate +} + +func (c *Client) RevokeClientID(ctx context.Context, cid, email string) error { + _, err := c.cidb.Set(ctx, cid, email, RefreshTokenExpiry).Result() + if err != nil { + return err + } + return nil +} + +func (c *Client) RevokeRefreshToken(ctx context.Context, token string) error { + _, err := c.rtdb.Set(ctx, token, nil, RefreshTokenExpiry).Result() + return err +} + +func (c *Client) ValidateClientID(ctx context.Context, cid string) error { + // check if a key with client id exists + // if the key exists it means that the client id is revoked and token should be denied + // we don't need the email value here + _, err := c.cidb.Get(ctx, cid).Result() + if err != nil { + switch err { + case redis.Nil: + return nil + default: + return ErrRTValidate + } + } + + return ErrRTValidate +} + +func ParseRefreshTokenClaims(token string) (jwt.Token, error) { return jwt.Parse([]byte(token)) } + +const ( + RefreshTokenExpiry = time.Hour * 24 * 7 + AccessTokenExpiry = time.Minute * 5 +) From 2d7673a8289711b1c8b10a89036c74581fa59be4 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 19:38:52 +0100 Subject: [PATCH 141/246] suid.MustParse --- internal/suid/suid.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/suid/suid.go b/internal/suid/suid.go index c6b93c87..f4fa6727 100644 --- a/internal/suid/suid.go +++ b/internal/suid/suid.go @@ -23,6 +23,14 @@ func ParseString(s string) (UUID, error) { return UUID{uid}, err } +func MustParse(s string) UUID { + uid, err := ParseString(s) + if err != nil { + panic(err) + } + return uid +} + func (s SUID) String() string { return string(s) } func (s SUID) UUID() (UUID, error) { From a8079b55b6a93c879063de172cbe0386123de523 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 19:39:37 +0100 Subject: [PATCH 142/246] rearrange handleRefreshToken --- internal/internal.go | 15 +++++++++- service/auth/routes.go | 8 +++--- service/auth/v2/service.go | 58 ++++++++++++++++++++++++++++++++++---- 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/internal/internal.go b/internal/internal.go index a610bceb..990fd265 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -6,6 +6,7 @@ import ( "net/mail" "strings" + "github.com/lestrrat-go/jwx/v2/jwt" "github.com/rog-golang-buddies/rmx/internal/suid" gpv "github.com/wagslane/go-password-validator" "golang.org/x/crypto/bcrypt" @@ -23,7 +24,8 @@ var ( type ContextKey string const ( - EmailKey = ContextKey("rmx-email") + EmailKey = ContextKey("email-key") + TokenKey = ContextKey("token-key") ) type MsgTyp int @@ -95,6 +97,17 @@ func (t *MsgTyp) MarshalJSON() ([]byte, error) { return []byte(sb.String()), nil } +type TokenClient interface { + RTokenClient + WTokenClient +} + +type RTokenClient interface { + Validate(ctx context.Context, token jwt.Token) error +} + +type WTokenClient interface{} + type UserRepo interface { RUserRepo WUserRepo diff --git a/service/auth/routes.go b/service/auth/routes.go index 37d76b93..65450594 100644 --- a/service/auth/routes.go +++ b/service/auth/routes.go @@ -139,10 +139,6 @@ func (s *Service) handleRefreshToken(key jwk.Key) http.HandlerFunc { return } - if err := s.arc.ValidateRefreshToken(context.Background(), rtc.Value); err != nil { - s.respond(w, r, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - } - tc, err := auth.ParseRefreshTokenWithValidate(&key, rtc.Value) if err != nil { s.respond(w, r, err, http.StatusUnauthorized) @@ -155,6 +151,10 @@ func (s *Service) handleRefreshToken(key jwk.Key) http.HandlerFunc { return } + if err := s.arc.ValidateRefreshToken(context.Background(), rtc.Value); err != nil { + s.respond(w, r, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + } + // use the same Client ID since it's a refresh token request cid := tc.Subject() diff --git a/service/auth/v2/service.go b/service/auth/v2/service.go index 4df307a2..5ef57ad1 100644 --- a/service/auth/v2/service.go +++ b/service/auth/v2/service.go @@ -10,6 +10,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" h "github.com/hyphengolang/prelude/http" @@ -55,7 +56,7 @@ func (s *Service) routes() { r.Delete("/sign-out", s.handleSignOut()) r.Post("/sign-up", s.handleSignUp()) - r.Get("/refresh", s.handleRefresh(key.Private())) + r.Get("/refresh", s.handleRefresh(key.Private())) // auth middleware }) s.m.Route("/api/v2/account", func(r chi.Router) { @@ -70,14 +71,48 @@ func (s *Service) handleRefresh(key jwk.Key) http.HandlerFunc { } return func(w http.ResponseWriter, r *http.Request) { - c, err := r.Cookie(auth.RefreshTokenCookieName) - if err != nil { + j, ok := r.Context().Value(internal.TokenKey).(jwt.Token) + if !ok { + s.respond(w, r, nil, http.StatusInternalServerError) + return + } + + e, ok := r.Context().Value(internal.EmailKey).(internal.Email) + if !ok { + s.respond(w, r, nil, http.StatusInternalServerError) + return + } + + // redis validation + if err := s.t.Validate(r.Context(), j); err != nil { s.respond(w, r, err, http.StatusUnauthorized) return } + id, _ := suid.ParseString(j.Subject()) + + _, ats, rts, err := s.signedTokens(key, e.String(), id) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + c := &http.Cookie{ + Path: "/", + Name: cookieName, + Value: string(rts), + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteLaxMode, + MaxAge: int(auth.RefreshTokenExpiry), + } + + tk := &token{ + AccessToken: string(ats), + } + s.setCookie(w, c) - s.respond(w, r, nil, http.StatusNotImplemented) + s.respond(w, r, tk, http.StatusOK) } } @@ -127,7 +162,7 @@ func (s *Service) handleSignIn(privateKey jwk.Key) http.HandlerFunc { c := &http.Cookie{ Path: "/", - Name: auth.RefreshTokenCookieName, + Name: cookieName, Value: string(rts), HttpOnly: true, Secure: r.TLS != nil, @@ -149,7 +184,7 @@ func (s *Service) handleSignOut() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { c := &http.Cookie{ Path: "/", - Name: auth.RefreshTokenCookieName, + Name: cookieName, HttpOnly: true, // Secure: r.TLS != nil, // SameSite: http.SameSiteLaxMode, @@ -182,7 +217,9 @@ type Service struct { ctx context.Context m chi.Router + r internal.UserRepo + t internal.TokenClient log func(...any) logf func(string, ...any) @@ -249,11 +286,14 @@ func (s *Service) signedTokens(key jwk.Key, email string, uuid suid.UUID) (its, func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } func NewService(ctx context.Context, m chi.Router, r internal.UserRepo) *Service { + var t internal.TokenClient + s := &Service{ ctx, m, r, + t, log.Println, log.Printf, @@ -280,3 +320,9 @@ type User struct { Username string `json:"username"` Password internal.Password `json:"password"` } + +const ( + cookieName = "RMX_REFRESH_TOKEN" + refreshExp = time.Hour * 24 * 7 + accessExp = time.Minute * 5 +) From 2bae747b0e4734aea66db0fa7e314ed6c52fb80c Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 20:35:05 +0100 Subject: [PATCH 143/246] middlware tests for Autherization/Cookie headers --- internal/auth/auth.go | 20 ++++++++----- internal/auth/auth_test.go | 60 +++++++++++++++++++++++--------------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index e4b5e572..0bd31c90 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -235,7 +235,7 @@ const ( */ func Authentication(algo jwa.SignatureAlgorithm, key jwk.Key, cookieName ...string) func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { - at := func(w http.ResponseWriter, r *http.Request) { + var at http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { token, err := jwt.ParseRequest(r, jwt.WithKey(algo, key)) if err != nil { w.WriteHeader(http.StatusUnauthorized) @@ -254,8 +254,14 @@ func Authentication(algo jwa.SignatureAlgorithm, key jwk.Key, cookieName ...stri h.ServeHTTP(w, r) } - rt := func(w http.ResponseWriter, r *http.Request) { - token, err := jwt.ParseRequest(r, jwt.WithKey(algo, key), jwt.WithHeaderKey(cookieName[0])) + var rt http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + rc, err := r.Cookie(cookieName[0]) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + token, err := jwt.Parse([]byte(rc.Value), jwt.WithKey(algo, key), jwt.WithValidate(true)) if err != nil { w.WriteHeader(http.StatusUnauthorized) return @@ -268,8 +274,8 @@ func Authentication(algo jwa.SignatureAlgorithm, key jwk.Key, cookieName ...stri return } - r = r.WithContext(context.WithValue(r.Context(), internal.EmailKey, internal.Email(email))) - r = r.WithContext(context.WithValue(r.Context(), internal.TokenKey, token)) + ctx := context.WithValue(r.Context(), internal.EmailKey, internal.Email(email)) + r = r.WithContext(context.WithValue(ctx, internal.TokenKey, token)) h.ServeHTTP(w, r) } @@ -311,7 +317,7 @@ func Authenticate(algo jwa.SignatureAlgorithm, key jwk.Key) func(http.Handler) h // Authenticate against the refreshToken func AuthenticateRefresh(algo jwa.SignatureAlgorithm, key jwk.Key, cookieName string) func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { - var f http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { + var refresh http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { rc, err := r.Cookie(cookieName) if err != nil { w.WriteHeader(http.StatusUnauthorized) @@ -338,6 +344,6 @@ func AuthenticateRefresh(algo jwa.SignatureAlgorithm, key jwk.Key, cookieName st h.ServeHTTP(w, r) } - return http.HandlerFunc(f) + return http.HandlerFunc(refresh) } } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index e0a6211a..c66eb86f 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -8,30 +8,13 @@ import ( "time" "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwt" "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/is" "github.com/rog-golang-buddies/rmx/internal/suid" ) -var rsaPrivateKey = `-----BEGIN PRIVATE KEY----- -MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAML5MHFgqUlZcENS -hHZ83yXfoUpqaMfp5/UdgMIJ0S5DW5QEON6reAsDu6zP0BEVZhg65pEYWEraBrGK -Vcbx7dsVqK4Z0GMm0YRAvB+1K+pYlXwld90mwG1TqOKDPQXqC0Z/jZi6DSsAhfJU -WN0rkInZRtoVeRzbbh+nLN8nd14fAgMBAAECgYEAor+A2VL3XBvFIt0RZxpq5mFa -cBSMrDsqfSeIX+/z5SsimVZA5lW5GXCfSuwY4Pm8xAL+jSUGJk0CA1bWrP8rLByS -cQAy1q0odaAiWIG5zFUEQBg5Q5b3+jXmh2zwtO7yhPuXn1/vBGg+FvyR57gV/3F+ -TuBfR6Bc3VWKuj7Gm5kCQQDuRgm8HTDbX7IQ0EFAVKB73Pj4Gx5u2NieD9U8+qXx -JsAdn1vRvQ3mNJDR5OcTr4rPkpLLCtzjA2iTDXp4yqmrAkEA0Xp91LCpImKAOtM3 -4SGXdzKi9+7fWmcTtfkz996y9A1C9l27Cj92P7OFdwMB4Z/ZMizJd0eXYhXr4IxH -wBoxXQJAUBOXp/HDfqZdiIsEsuL+AEKWJYOvqZ8UxaIajuDJrg7Q1+O7jvRTXH9k -ADZGdnYzV2kyDiy7aUu29Fy+QSQS+wJAJyEsdBhz35pqvZJK8+DkfD2XN50FV8u9 -YNamIH0XDIOVqJOlpqpoGkocejizl0PWvIqlL4TOAGJ75zwNAxNheQJABEA2/hfF -GMJsOrnD74rGP/Lfpg882AmeUoT5eH766sSobFfUYJZvyAmnQoK2Lzg2hrKwXXix -JvEGfrhihVLb7g== ------END PRIVATE KEY----- -` - func TestToken(t *testing.T) { t.Parallel() is := is.New(t) @@ -89,7 +72,7 @@ func TestMiddleware(t *testing.T) { ats, err := SignToken(key.Private(), &opt) is.NoErr(err) // signing access token - h := Authenticate(opt.Algo, key.Public())(http.NotFoundHandler()) + h := Authentication(opt.Algo, key.Public())(http.NotFoundHandler()) req, _ := http.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(ats))) @@ -97,13 +80,13 @@ func TestMiddleware(t *testing.T) { res := httptest.NewRecorder() h.ServeHTTP(res, req) - is.Equal(res.Result().StatusCode, http.StatusNotFound) // return not found + is.Equal(res.Result().StatusCode, http.StatusNotFound) // http page not found }) t.Run("authenticate against Cookie header", func(t *testing.T) { key := NewPairES256() - e := internal.Email("foobar@gmail.com") + e, cookieName := internal.Email("foobar@gmail.com"), `__myCookie` opt := TokenOption{ Issuer: "github.com/rog-golang-buddies/rmx", @@ -117,9 +100,7 @@ func TestMiddleware(t *testing.T) { rts, err := SignToken(key.Private(), &opt) is.NoErr(err) // signing refresh token - cookieName := `__myCookie` - - h := AuthenticateRefresh(opt.Algo, key.Public(), cookieName)(http.NotFoundHandler()) + h := Authentication(opt.Algo, key.Public(), cookieName)(http.NotFoundHandler()) req, _ := http.NewRequest(http.MethodGet, "/", nil) req.AddCookie(&http.Cookie{ @@ -137,4 +118,35 @@ func TestMiddleware(t *testing.T) { is.Equal(res.Result().StatusCode, http.StatusNotFound) // http page not found }) + t.Run("jwk parse request", func(t *testing.T) { + key := NewPairES256() + + e, cookieName := internal.Email("foobar@gmail.com"), `__g` + + opt := TokenOption{ + Issuer: "github.com/rog-golang-buddies/rmx", + Subject: suid.NewUUID().String(), + Expiration: time.Hour * 10, + Claims: []fp.Tuple{{"email", e.String()}}, + Algo: jwa.ES256, + } + + // rts + rts, err := SignToken(key.Private(), &opt) + is.NoErr(err) // signing refresh token + + c := &http.Cookie{ + Path: "/", + Name: cookieName, + Value: string(rts), + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + MaxAge: 24 * 7, + } + req, _ := http.NewRequest(http.MethodGet, "/", nil) + req.AddCookie(c) + + _, err = jwt.Parse([]byte(c.Value), jwt.WithKey(opt.Algo, key.Public()), jwt.WithValidate(true)) + is.NoErr(err) // parsing jwk page not found + }) } From f688d5b3492713203ebc8c54cd9d59ea0c708dd9 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 20:50:09 +0100 Subject: [PATCH 144/246] added testing for refresh inside service --- service/auth/v2/service.go | 13 +++++++------ service/auth/v2/service_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/service/auth/v2/service.go b/service/auth/v2/service.go index 5ef57ad1..d63db35e 100644 --- a/service/auth/v2/service.go +++ b/service/auth/v2/service.go @@ -56,11 +56,12 @@ func (s *Service) routes() { r.Delete("/sign-out", s.handleSignOut()) r.Post("/sign-up", s.handleSignUp()) - r.Get("/refresh", s.handleRefresh(key.Private())) // auth middleware + auth := r.With(auth.Authentication(jwa.ES256, key.Public(), cookieName)) + auth.Get("/refresh", s.handleRefresh(key.Private())) // auth middleware }) s.m.Route("/api/v2/account", func(r chi.Router) { - auth := r.With(auth.Authenticate(jwa.ES256, key.Public())) + auth := r.With(auth.Authentication(jwa.ES256, key.Public())) auth.Get("/me", s.handleIdentity()) }) } @@ -84,10 +85,10 @@ func (s *Service) handleRefresh(key jwk.Key) http.HandlerFunc { } // redis validation - if err := s.t.Validate(r.Context(), j); err != nil { - s.respond(w, r, err, http.StatusUnauthorized) - return - } + // if err := s.t.Validate(r.Context(), j); err != nil { + // s.respond(w, r, err, http.StatusUnauthorized) + // return + // } id, _ := suid.ParseString(j.Subject()) diff --git a/service/auth/v2/service_test.go b/service/auth/v2/service_test.go index 186cb24f..b680bc3d 100644 --- a/service/auth/v2/service_test.go +++ b/service/auth/v2/service_test.go @@ -71,4 +71,30 @@ func TestService(t *testing.T) { res, _ = srv.Client().Do(req) is.Equal(res.StatusCode, http.StatusOK) // delete cookie }) + + t.Run("refresh token", func(t *testing.T) { + payload := ` + { + "email":"fizz@gmail.com", + "password":"fizz_$PW_10" + }` + + res, _ := srv.Client().Post(srv.URL+"/api/v2/auth/sign-in", applicationJson, strings.NewReader(payload)) + is.Equal(res.StatusCode, http.StatusOK) // add refresh token + + // get the refresh token from the response's `Set-Cookie` header + c := &http.Cookie{} + for _, k := range res.Cookies() { + t.Log(k.Value) + if k.Name == cookieName { + c = k + } + } + + req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/v2/auth/refresh", nil) + req.AddCookie(c) + + res, _ = srv.Client().Do(req) + is.Equal(res.StatusCode, http.StatusOK) // refresh token + }) } From 79c0d2dfc401325d58f8a95cd042127b0fc225d8 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 21:12:04 +0100 Subject: [PATCH 145/246] auth is now in a single file --- service/auth/routes.go | 197 -------------- service/auth/service.go | 360 +++++++++++++++++++------- service/auth/{v2 => }/service_test.go | 0 service/auth/v2/service.go | 329 ----------------------- 4 files changed, 265 insertions(+), 621 deletions(-) delete mode 100644 service/auth/routes.go rename service/auth/{v2 => }/service_test.go (100%) delete mode 100644 service/auth/v2/service.go diff --git a/service/auth/routes.go b/service/auth/routes.go deleted file mode 100644 index 65450594..00000000 --- a/service/auth/routes.go +++ /dev/null @@ -1,197 +0,0 @@ -package auth - -import ( - "context" - "net/http" - - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/rog-golang-buddies/rmx/internal/dto" - "github.com/rog-golang-buddies/rmx/internal/suid" - "github.com/rog-golang-buddies/rmx/service/internal/auth" -) - -// Account Sign Up request handler -func (s *Service) handleSignUp() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var u dto.User - if err := s.decode(w, r, &u); err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - // check if the provided string is a valid email - if err := u.Email.Validate(); err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - // check if the password is strong enough - if err := u.Password.Validate(); err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - // hash the password to store in db - if err := u.HashPassword(); err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - if err := s.ur.Add(&u); err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - s.created(w, r, string(u.ID.ShortUUID())) - } -} - -// Account Sign In handler -func (s *Service) handleSignIn(key jwk.Key) http.HandlerFunc { - type loginUser struct { - Email dto.Email `json:"email"` - Password dto.Password `json:"password"` - } - - type authTokens struct { - IDToken string `json:"idToken"` - AccessToken string `json:"accessToken"` - } - - return func(w http.ResponseWriter, r *http.Request) { - var u loginUser - if err := s.decode(w, r, &u); err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - // Get users' information - ui, err := s.ur.LookupEmail(string(u.Email)) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - // Check if the provided password matches the users' - if err := ui.ComparePassword(string(u.Password)); err != nil { - s.respond(w, r, err, http.StatusUnauthorized) - return - } - - // new Client ID since it's a new login request for a new connection - cid := suid.NewSUID() - - // Generate new JWT tokens - its, ats, rts, err := s.signedTokens(key, string(ui.Email), cid.String()) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - // NOTE: Secure should be set to true in production - cookie := &http.Cookie{ - Path: "/", - Name: auth.RefreshTokenCookieName, - Value: string(rts), - HttpOnly: true, - Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - MaxAge: int(auth.RefreshTokenExpiry), - } - - v := &authTokens{ - IDToken: string(its), - AccessToken: string(ats), - } - - s.respondCookie(w, r, v, cookie) - } -} - -// Account Sign Out handler -// removes the Refresh Token by setting its MaxAge property to -1 -func (s *Service) handleSignOut() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // NOTE: Secure should be set to true in production - c := &http.Cookie{ - Path: "/", - Name: auth.RefreshTokenCookieName, - Value: "", - HttpOnly: true, - Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - MaxAge: -1, - } - - s.respondCookie(w, r, http.StatusText(http.StatusOK), c) - } -} - -func (s *Service) handleRefreshToken(key jwk.Key) http.HandlerFunc { - type response struct { - AccessToken string `json:"accessToken"` - } - - return func(w http.ResponseWriter, r *http.Request) { - rtc, err := r.Cookie(auth.RefreshTokenCookieName) - if err != nil { - s.respond(w, r, err, http.StatusUnauthorized) - return - } - - tc, err := auth.ParseRefreshTokenWithValidate(&key, rtc.Value) - if err != nil { - s.respond(w, r, err, http.StatusUnauthorized) - return - } - - email, ok := tc.PrivateClaims()["email"].(string) - if !ok { - s.respond(w, r, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - return - } - - if err := s.arc.ValidateRefreshToken(context.Background(), rtc.Value); err != nil { - s.respond(w, r, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - } - - // use the same Client ID since it's a refresh token request - cid := tc.Subject() - - _, ats, rts, err := s.signedTokens(key, email, cid) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - c := &http.Cookie{ - Path: "/", - Name: auth.RefreshTokenCookieName, - Value: string(rts), - HttpOnly: true, - Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - MaxAge: int(auth.RefreshTokenExpiry), - } - - v := &response{ - AccessToken: string(ats), - } - - s.respondCookie(w, r, v, c) - } -} - -func (s *Service) handleUserInfo() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - email := r.Context().Value(auth.EmailKey).(string) - - u, err := s.ur.LookupEmail(email) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - s.respond(w, r, u, http.StatusOK) - } -} diff --git a/service/auth/service.go b/service/auth/service.go index 5716c0b6..46fca512 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -1,159 +1,329 @@ package auth import ( + "context" + "errors" "log" "net/http" "time" - h "github.com/hyphengolang/prelude/http" + "github.com/go-chi/chi/v5" + "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/go-chi/chi/v5" - "github.com/rog-golang-buddies/rmx/internal/dto" + h "github.com/hyphengolang/prelude/http" + + // github.com/rog-golang-buddies/rmx/service/internal/auth/auth + "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/auth" "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/suid" - "github.com/rog-golang-buddies/rmx/service/internal/auth" - "github.com/rog-golang-buddies/rmx/service/internal/middlewares" - "github.com/rog-golang-buddies/rmx/test/mock" + // big no-no +) + +var ( + ErrNoCookie = errors.New("user: cookie not found") + ErrSessionNotFound = errors.New("user: session not found") + ErrSessionExists = errors.New("user: session already exists") ) /* -// TODO use os/viper to get `key.pem` body -var secretTest = `-----BEGIN PRIVATE KEY----- -MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAML5MHFgqUlZcENS -hHZ83yXfoUpqaMfp5/UdgMIJ0S5DW5QEON6reAsDu6zP0BEVZhg65pEYWEraBrGK -Vcbx7dsVqK4Z0GMm0YRAvB+1K+pYlXwld90mwG1TqOKDPQXqC0Z/jZi6DSsAhfJU -WN0rkInZRtoVeRzbbh+nLN8nd14fAgMBAAECgYEAor+A2VL3XBvFIt0RZxpq5mFa -cBSMrDsqfSeIX+/z5SsimVZA5lW5GXCfSuwY4Pm8xAL+jSUGJk0CA1bWrP8rLByS -cQAy1q0odaAiWIG5zFUEQBg5Q5b3+jXmh2zwtO7yhPuXn1/vBGg+FvyR57gV/3F+ -TuBfR6Bc3VWKuj7Gm5kCQQDuRgm8HTDbX7IQ0EFAVKB73Pj4Gx5u2NieD9U8+qXx -JsAdn1vRvQ3mNJDR5OcTr4rPkpLLCtzjA2iTDXp4yqmrAkEA0Xp91LCpImKAOtM3 -4SGXdzKi9+7fWmcTtfkz996y9A1C9l27Cj92P7OFdwMB4Z/ZMizJd0eXYhXr4IxH -wBoxXQJAUBOXp/HDfqZdiIsEsuL+AEKWJYOvqZ8UxaIajuDJrg7Q1+O7jvRTXH9k -ADZGdnYzV2kyDiy7aUu29Fy+QSQS+wJAJyEsdBhz35pqvZJK8+DkfD2XN50FV8u9 -YNamIH0XDIOVqJOlpqpoGkocejizl0PWvIqlL4TOAGJ75zwNAxNheQJABEA2/hfF -GMJsOrnD74rGP/Lfpg882AmeUoT5eH766sSobFfUYJZvyAmnQoK2Lzg2hrKwXXix -JvEGfrhihVLb7g== ------END PRIVATE KEY----- -` +Register a new user + + [?] POST /auth/sign-up + +Get current account identity + + [?] GET /account/me + +Create a cookie + + [?] POST /auth/sign-in + +Delete a cookie + + [?] DELETE /auth/sign-out + +Refresh token + + [?] GET /auth/refresh */ +func (s *Service) routes() { + key := auth.NewPairES256() -type Service struct { - m chi.Router - ur dto.UserRepo + s.m.Route("/api/v2/auth", func(r chi.Router) { + r.Post("/sign-in", s.handleSignIn(key.Private())) + r.Delete("/sign-out", s.handleSignOut()) + r.Post("/sign-up", s.handleSignUp()) - l *log.Logger + auth := r.With(auth.Authentication(jwa.ES256, key.Public(), cookieName)) // passing cookie is required + auth.Get("/refresh", s.handleRefresh(key.Private())) + }) - arc *auth.Client + s.m.Route("/api/v2/account", func(r chi.Router) { + auth := r.With(auth.Authentication(jwa.ES256, key.Public())) + auth.Get("/me", s.handleIdentity()) + }) } -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } +func (s *Service) handleRefresh(key jwk.Key) http.HandlerFunc { + type token struct { + AccessToken string `json:"accessToken"` + } -func NewService(m chi.Router, r dto.UserRepo) *Service { - s := &Service{m: m, ur: r, l: log.Default()} - s.routes() - return s + return func(w http.ResponseWriter, r *http.Request) { + + // this have to exist else it would have lead to + // a 401 response. this is risky and will debate on + // whether we should be more cautious + j, _ := r.Context().Value(internal.TokenKey).(jwt.Token) + e, _ := r.Context().Value(internal.EmailKey).(internal.Email) + + // already checked in auth but I am too tired + // to come up with a cleaner solution + k, _ := r.Cookie(cookieName) + + err := s.t.ValidateRefreshToken(r.Context(), k.Value) + if err != nil { + s.respond(w, r, err, http.StatusTeapot) + return + } + + id, _ := suid.ParseString(j.Subject()) + + _, ats, rts, err := s.signedTokens(key, e.String(), id) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + c := &http.Cookie{ + Path: "/", + Name: cookieName, + Value: string(rts), + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteLaxMode, + MaxAge: int(auth.RefreshTokenExpiry), + } + + tk := &token{ + AccessToken: string(ats), + } + + s.setCookie(w, c) + s.respond(w, r, tk, http.StatusOK) + } } -func DefaultService() *Service { - s := &Service{m: chi.NewMux(), ur: mock.UserRepo(), l: log.Default()} - s.routes() - return s +func (s *Service) handleIdentity() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + email := r.Context().Value(internal.EmailKey).(internal.Email) + + u, err := s.r.Select(r.Context(), email) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + s.respond(w, r, u, http.StatusOK) + } +} + +func (s *Service) handleSignIn(privateKey jwk.Key) http.HandlerFunc { + type token struct { + IDToken string `json:"idToken"` + AccessToken string `json:"accessToken"` + } + + return func(w http.ResponseWriter, r *http.Request) { + var dto User + if err := s.decode(w, r, &dto); err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + u, err := s.r.Select(r.Context(), dto.Email) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + if err := u.Password.Compare(dto.Password.String()); err != nil { + s.respond(w, r, err, http.StatusUnauthorized) + return + } + + its, ats, rts, err := s.signedTokens(privateKey, u.Email.String(), suid.NewUUID()) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + c := &http.Cookie{ + Path: "/", + Name: cookieName, + Value: string(rts), + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteLaxMode, + MaxAge: int(auth.RefreshTokenExpiry), + } + + tk := &token{ + IDToken: string(its), + AccessToken: string(ats), + } + + s.setCookie(w, c) + s.respond(w, r, tk, http.StatusOK) + } } -func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { - h.Respond(w, r, data, status) +func (s *Service) handleSignOut() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + c := &http.Cookie{ + Path: "/", + Name: cookieName, + HttpOnly: true, + // Secure: r.TLS != nil, + // SameSite: http.SameSiteLaxMode, + MaxAge: -1, + } + + s.setCookie(w, c) + s.respond(w, r, http.StatusText(http.StatusOK), http.StatusOK) + } } -func (s *Service) respondCookie(w http.ResponseWriter, r *http.Request, data any, c *http.Cookie) { - http.SetCookie(w, c) - s.respond(w, r, data, http.StatusOK) +func (s *Service) handleSignUp() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var u internal.User + if err := s.newUser(w, r, &u); err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + if err := s.r.Insert(r.Context(), &u); err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + s.created(w, r, u.ID.ShortUUID().String()) + } } -func (s *Service) created(w http.ResponseWriter, r *http.Request, id string) { - h.Created(w, r, id) +type Service struct { + ctx context.Context + + m chi.Router + + r internal.UserRepo + t internal.TokenClient + + log func(...any) + logf func(string, ...any) + + decode func(http.ResponseWriter, *http.Request, any) error + respond func(http.ResponseWriter, *http.Request, any, int) + created func(http.ResponseWriter, *http.Request, string) + setCookie func(http.ResponseWriter, *http.Cookie) } -func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { - return h.Decode(w, r, data) +func (s *Service) newUser(w http.ResponseWriter, r *http.Request, u *internal.User) (err error) { + var dto User + if err = s.decode(w, r, &dto); err != nil { + return + } + + var h internal.PasswordHash + h, err = dto.Password.Hash() + if err != nil { + return + } + + *u = internal.User{ + ID: suid.NewUUID(), + Username: dto.Username, + Email: dto.Email, + Password: h, + } + + return nil } func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { return suid.ParseString(chi.URLParam(r, "uuid")) } -func (s *Service) signedTokens(key jwk.Key, email, uuid string) (its, ats, rts []byte, err error) { - // new client ID for tracking user connections - cid := suid.NewSUID() +// TODO there is two cid's being used here, need clarification +func (s *Service) signedTokens(key jwk.Key, email string, uuid suid.UUID) (its, ats, rts []byte, err error) { opt := auth.TokenOption{ Issuer: "github.com/rog-golang-buddies/rmx", - Subject: cid.String(), + Subject: uuid.String(), // new client ID for tracking user connections Expiration: time.Hour * 10, Claims: []fp.Tuple{{"email", email}}, + Algo: jwa.ES256, } - if its, err = auth.SignToken(&key, &opt); err != nil { + if its, err = auth.SignToken(key, &opt); err != nil { return nil, nil, nil, err } - opt.Subject = uuid - opt.Expiration = auth.AccessTokenExpiry - if ats, err = auth.SignToken(&key, &opt); err != nil { + opt.Expiration = time.Minute * 5 + if ats, err = auth.SignToken(key, &opt); err != nil { return nil, nil, nil, err } - opt.Expiration = auth.RefreshTokenExpiry - if rts, err = auth.SignToken(&key, &opt); err != nil { + opt.Expiration = time.Hour * 24 * 7 + if rts, err = auth.SignToken(key, &opt); err != nil { return nil, nil, nil, err } return its, ats, rts, nil } -/* -type SignupUser struct { - Email dto.Email `json:"email"` - Username string `json:"username"` - Password dto.Password `json:"password"` -} +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func (v *SignupUser) decode(iu *dto.User) error { - h, err := v.Password.Hash() - if err != nil { - return err - } +func NewService(ctx context.Context, m chi.Router, r internal.UserRepo) *Service { + var t = auth.DefaultClient - *iu = dto.User{ - ID: suid.NewUUID(), - Email: v.Email, - Username: v.Username, - Password: h, - } + s := &Service{ + ctx, - return nil -} -*/ + m, + r, + t, -func (s *Service) routes() { - // initialize redis store - s.arc = auth.NewRedis("localhost:6379", "") + log.Println, + log.Printf, - // panic should be ok as we need this to return no error - // else it'll completely break our auth model - priv, pub, err := auth.GenerateKeys() - if err != nil { - s.l.Fatalln(err) + h.Decode, + h.Respond, + h.Created, + http.SetCookie, } - s.m.Route("/api/v1/auth", func(r chi.Router) { - r.Post("/register", s.handleSignUp()) - r.Post("/login", s.handleSignIn(pub)) - r.Get("/refresh", s.handleRefreshToken(priv)) - r.Get("/logout", s.handleSignOut()) - }) + s.routes() + return s +} - s.m.Route("/api/v1/account", func(r chi.Router) { - r.Use(middlewares.Authenticate(pub)) - r.Get("/me", s.handleUserInfo()) - }) +func (s *Service) Context() context.Context { + if s.ctx != nil { + return s.ctx + } + return context.Background() +} + +type User struct { + Email internal.Email `json:"email"` + Username string `json:"username"` + Password internal.Password `json:"password"` } + +const ( + cookieName = "RMX_REFRESH_TOKEN" + refreshExp = time.Hour * 24 * 7 + accessExp = time.Minute * 5 +) diff --git a/service/auth/v2/service_test.go b/service/auth/service_test.go similarity index 100% rename from service/auth/v2/service_test.go rename to service/auth/service_test.go diff --git a/service/auth/v2/service.go b/service/auth/v2/service.go deleted file mode 100644 index d63db35e..00000000 --- a/service/auth/v2/service.go +++ /dev/null @@ -1,329 +0,0 @@ -package auth - -import ( - "context" - "errors" - "log" - "net/http" - "time" - - "github.com/go-chi/chi/v5" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" - - h "github.com/hyphengolang/prelude/http" - - "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/auth" - "github.com/rog-golang-buddies/rmx/internal/fp" - "github.com/rog-golang-buddies/rmx/internal/suid" - // big no-no -) - -var ( - ErrNoCookie = errors.New("user: cookie not found") - ErrSessionNotFound = errors.New("user: session not found") - ErrSessionExists = errors.New("user: session already exists") -) - -/* -Register a new user - - [?] POST /auth/sign-up - -Get current account identity - - [?] GET /account/me - -Create a cookie - - [?] POST /auth/sign-in - -Delete a cookie - - [?] DELETE /auth/sign-out - -Refresh token - - [ ] GET /auth/refresh -*/ -func (s *Service) routes() { - key := auth.NewPairES256() - - s.m.Route("/api/v2/auth", func(r chi.Router) { - r.Post("/sign-in", s.handleSignIn(key.Private())) - r.Delete("/sign-out", s.handleSignOut()) - r.Post("/sign-up", s.handleSignUp()) - - auth := r.With(auth.Authentication(jwa.ES256, key.Public(), cookieName)) - auth.Get("/refresh", s.handleRefresh(key.Private())) // auth middleware - }) - - s.m.Route("/api/v2/account", func(r chi.Router) { - auth := r.With(auth.Authentication(jwa.ES256, key.Public())) - auth.Get("/me", s.handleIdentity()) - }) -} - -func (s *Service) handleRefresh(key jwk.Key) http.HandlerFunc { - type token struct { - AccessToken string `json:"accessToken"` - } - - return func(w http.ResponseWriter, r *http.Request) { - j, ok := r.Context().Value(internal.TokenKey).(jwt.Token) - if !ok { - s.respond(w, r, nil, http.StatusInternalServerError) - return - } - - e, ok := r.Context().Value(internal.EmailKey).(internal.Email) - if !ok { - s.respond(w, r, nil, http.StatusInternalServerError) - return - } - - // redis validation - // if err := s.t.Validate(r.Context(), j); err != nil { - // s.respond(w, r, err, http.StatusUnauthorized) - // return - // } - - id, _ := suid.ParseString(j.Subject()) - - _, ats, rts, err := s.signedTokens(key, e.String(), id) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - c := &http.Cookie{ - Path: "/", - Name: cookieName, - Value: string(rts), - HttpOnly: true, - Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - MaxAge: int(auth.RefreshTokenExpiry), - } - - tk := &token{ - AccessToken: string(ats), - } - - s.setCookie(w, c) - s.respond(w, r, tk, http.StatusOK) - } -} - -func (s *Service) handleIdentity() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - email := r.Context().Value(internal.EmailKey).(internal.Email) - - u, err := s.r.Select(r.Context(), email) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - s.respond(w, r, u, http.StatusOK) - } -} - -func (s *Service) handleSignIn(privateKey jwk.Key) http.HandlerFunc { - type token struct { - IDToken string `json:"idToken"` - AccessToken string `json:"accessToken"` - } - - return func(w http.ResponseWriter, r *http.Request) { - var dto User - if err := s.decode(w, r, &dto); err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - u, err := s.r.Select(r.Context(), dto.Email) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - if err := u.Password.Compare(dto.Password.String()); err != nil { - s.respond(w, r, err, http.StatusUnauthorized) - return - } - - its, ats, rts, err := s.signedTokens(privateKey, u.Email.String(), suid.NewUUID()) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - c := &http.Cookie{ - Path: "/", - Name: cookieName, - Value: string(rts), - HttpOnly: true, - Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - MaxAge: int(auth.RefreshTokenExpiry), - } - - tk := &token{ - IDToken: string(its), - AccessToken: string(ats), - } - - s.setCookie(w, c) - s.respond(w, r, tk, http.StatusOK) - } -} - -func (s *Service) handleSignOut() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - c := &http.Cookie{ - Path: "/", - Name: cookieName, - HttpOnly: true, - // Secure: r.TLS != nil, - // SameSite: http.SameSiteLaxMode, - MaxAge: -1, - } - - s.setCookie(w, c) - s.respond(w, r, http.StatusText(http.StatusOK), http.StatusOK) - } -} - -func (s *Service) handleSignUp() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var u internal.User - if err := s.newUser(w, r, &u); err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - if err := s.r.Insert(r.Context(), &u); err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - s.created(w, r, u.ID.ShortUUID().String()) - } -} - -type Service struct { - ctx context.Context - - m chi.Router - - r internal.UserRepo - t internal.TokenClient - - log func(...any) - logf func(string, ...any) - - decode func(http.ResponseWriter, *http.Request, any) error - respond func(http.ResponseWriter, *http.Request, any, int) - created func(http.ResponseWriter, *http.Request, string) - setCookie func(http.ResponseWriter, *http.Cookie) -} - -func (s *Service) newUser(w http.ResponseWriter, r *http.Request, u *internal.User) (err error) { - var dto User - if err = s.decode(w, r, &dto); err != nil { - return - } - - var h internal.PasswordHash - h, err = dto.Password.Hash() - if err != nil { - return - } - - *u = internal.User{ - ID: suid.NewUUID(), - Username: dto.Username, - Email: dto.Email, - Password: h, - } - - return nil -} - -func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { - return suid.ParseString(chi.URLParam(r, "uuid")) -} - -// TODO there is two cid's being used here, need clarification -func (s *Service) signedTokens(key jwk.Key, email string, uuid suid.UUID) (its, ats, rts []byte, err error) { - opt := auth.TokenOption{ - Issuer: "github.com/rog-golang-buddies/rmx", - Subject: uuid.String(), // new client ID for tracking user connections - Expiration: time.Hour * 10, - Claims: []fp.Tuple{{"email", email}}, - Algo: jwa.ES256, - } - - if its, err = auth.SignToken(key, &opt); err != nil { - return nil, nil, nil, err - } - - opt.Expiration = time.Minute * 5 - if ats, err = auth.SignToken(key, &opt); err != nil { - return nil, nil, nil, err - } - - opt.Expiration = time.Hour * 24 * 7 - if rts, err = auth.SignToken(key, &opt); err != nil { - return nil, nil, nil, err - } - - return its, ats, rts, nil -} - -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } - -func NewService(ctx context.Context, m chi.Router, r internal.UserRepo) *Service { - var t internal.TokenClient - - s := &Service{ - ctx, - - m, - r, - t, - - log.Println, - log.Printf, - - h.Decode, - h.Respond, - h.Created, - http.SetCookie, - } - - s.routes() - return s -} - -func (s *Service) Context() context.Context { - if s.ctx != nil { - return s.ctx - } - return context.Background() -} - -type User struct { - Email internal.Email `json:"email"` - Username string `json:"username"` - Password internal.Password `json:"password"` -} - -const ( - cookieName = "RMX_REFRESH_TOKEN" - refreshExp = time.Hour * 24 * 7 - accessExp = time.Minute * 5 -) From 81d037910db287ad405e5615789ecbd6bb3e586f Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 21:29:40 +0100 Subject: [PATCH 146/246] cleaning up store/ --- service/@dep/.go | 207 +++++++++++++++++++++++ store/db/v2/db.go | 16 -- store/{db => sql}/schema/migration.sql | 0 store/{db => sql}/schema/user_query.sql | 0 store/{db => sql}/schema/user_schema.sql | 0 store/{db => sql}/user/db.go | 0 store/{db => sql}/user/models.go | 0 store/{db => sql}/user/repo.go | 0 store/{db => sql}/user/user_query.sql.go | 0 store/{db => }/v2/auth/auth.go | 0 store/{db => }/v2/user/user.go | 0 11 files changed, 207 insertions(+), 16 deletions(-) create mode 100644 service/@dep/.go delete mode 100644 store/db/v2/db.go rename store/{db => sql}/schema/migration.sql (100%) rename store/{db => sql}/schema/user_query.sql (100%) rename store/{db => sql}/schema/user_schema.sql (100%) rename store/{db => sql}/user/db.go (100%) rename store/{db => sql}/user/models.go (100%) rename store/{db => sql}/user/repo.go (100%) rename store/{db => sql}/user/user_query.sql.go (100%) rename store/{db => }/v2/auth/auth.go (100%) rename store/{db => }/v2/user/user.go (100%) diff --git a/service/@dep/.go b/service/@dep/.go new file mode 100644 index 00000000..bccb9a2d --- /dev/null +++ b/service/@dep/.go @@ -0,0 +1,207 @@ +package auth + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "time" + + "github.com/go-redis/redis/v9" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/pkg/errors" + "github.com/rog-golang-buddies/rmx/internal/fp" +) + +type Client struct { + rtdb, cidb *redis.Client +} + +var ( + ErrNotImplemented = errors.New("not implemented") + ErrGenerateKey = errors.New("failed to generate new ecdsa key pair") + ErrSignTokens = errors.New("failed to generate signed tokens") + ErrRTValidate = errors.New("failed to validate refresh token") +) + +func NewRedis(addr, password string) *Client { + rtdb := redis.Options{Addr: addr, Password: password, DB: 0} + cidb := redis.Options{Addr: addr, Password: password, DB: 1} + + c := &Client{redis.NewClient(&rtdb), redis.NewClient(&cidb)} + return c +} + +const ( + defaultAddr = "localhost:6379" + defaultPassword = "" +) + +var DefaultClient = &Client{ + rtdb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 0}), + cidb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 1}), +} + +func (c *Client) ValidateRefreshToken(ctx context.Context, token string) error { + tc, err := ParseRefreshTokenClaims(token) + if err != nil { + return err + } + email, ok := tc.PrivateClaims()["email"].(string) + if !ok { + return ErrRTValidate + } + + cid := tc.Subject() + if err := c.ValidateClientID(ctx, cid); err != nil { + return err + } + + if _, err := c.rtdb.Get(ctx, token).Result(); err != nil { + switch err { + case redis.Nil: + return nil + default: + return err + } + } + + err = c.RevokeClientID(ctx, cid, email) + if err != nil { + return err + } + + return ErrRTValidate +} + +func (c *Client) RevokeClientID(ctx context.Context, cid, email string) error { + _, err := c.cidb.Set(ctx, cid, email, RefreshTokenExpiry).Result() + if err != nil { + return err + } + return nil +} + +func (c *Client) RevokeRefreshToken(ctx context.Context, token string) error { + _, err := c.rtdb.Set(ctx, token, nil, RefreshTokenExpiry).Result() + return err +} + +func (c *Client) ValidateClientID(ctx context.Context, cid string) error { + // check if a key with client id exists + // if the key exists it means that the client id is revoked and token should be denied + // we don't need the email value here + _, err := c.cidb.Get(ctx, cid).Result() + if err != nil { + switch err { + case redis.Nil: + return nil + default: + return ErrRTValidate + } + } + + return ErrRTValidate +} + +/* +// would like to find an alternative to using `os` package +func LoadPEM(path string) (private, public jwk.Key, err error) { + buf, err := os.ReadFile(path) + if err != nil { + return + } + + return GenerateKeys(string(buf)) +} +*/ + +func GenerateKeys() (jwk.Key, jwk.Key, error) { + private, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + + key, err := jwk.FromRaw(private) + if err != nil { + return nil, nil, err + } + + _, ok := key.(jwk.ECDSAPrivateKey) + if !ok { + return nil, nil, ErrGenerateKey + } + + pub, err := key.PublicKey() + if err != nil { + return nil, nil, err + } + + return key, pub, nil +} + +func SignToken(key *jwk.Key, opt *TokenOption) ([]byte, error) { + var t time.Time + if opt.IssuedAt.IsZero() { + t = time.Now().UTC() + } else { + t = opt.IssuedAt + } + + token, err := jwt.NewBuilder(). + Issuer(opt.Issuer). + Audience(opt.Audience). + Subject(opt.Subject). + IssuedAt(t). + Expiration(t.Add(opt.Expiration)). + Build() + if err != nil { + return nil, ErrSignTokens + } + + for _, c := range opt.Claims { + if !c.HasValue() { + return nil, fp.ErrTuple + } + + err := token.Set(c[0], c[1]) + if err != nil { + return nil, ErrSignTokens + } + } + + return jwt.Sign(token, jwt.WithKey(jwa.ES256, key)) +} + +func ParseRefreshTokenClaims(token string) (jwt.Token, error) { return jwt.Parse([]byte(token)) } + +func ParseRefreshTokenWithValidate(key *jwk.Key, token string) (jwt.Token, error) { + payload, err := jwt.Parse([]byte(token), + jwt.WithKey(jwa.ES256, key), + jwt.WithValidate(true)) + if err != nil { + return nil, err + } + + return payload, nil +} + +type TokenOption struct { + Issuer string + Audience []string + Subject string + Claims []fp.Tuple + IssuedAt time.Time + Expiration time.Duration +} + +type authCtxKey string + +const ( + RefreshTokenCookieName = "RMX_REFRESH_TOKEN" + RefreshTokenExpiry = time.Hour * 24 * 7 + AccessTokenExpiry = time.Minute * 5 + EmailKey = authCtxKey("rmx-email") +) diff --git a/store/db/v2/db.go b/store/db/v2/db.go deleted file mode 100644 index a6836672..00000000 --- a/store/db/v2/db.go +++ /dev/null @@ -1,16 +0,0 @@ -package user - -import ( - "context" - - "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/dto" -) - -var MapRepo = &repo{} - -type repo struct{} - -func (r repo) Insert(ctx context.Context, u *internal.User) error { - return dto.ErrNotImplemented -} diff --git a/store/db/schema/migration.sql b/store/sql/schema/migration.sql similarity index 100% rename from store/db/schema/migration.sql rename to store/sql/schema/migration.sql diff --git a/store/db/schema/user_query.sql b/store/sql/schema/user_query.sql similarity index 100% rename from store/db/schema/user_query.sql rename to store/sql/schema/user_query.sql diff --git a/store/db/schema/user_schema.sql b/store/sql/schema/user_schema.sql similarity index 100% rename from store/db/schema/user_schema.sql rename to store/sql/schema/user_schema.sql diff --git a/store/db/user/db.go b/store/sql/user/db.go similarity index 100% rename from store/db/user/db.go rename to store/sql/user/db.go diff --git a/store/db/user/models.go b/store/sql/user/models.go similarity index 100% rename from store/db/user/models.go rename to store/sql/user/models.go diff --git a/store/db/user/repo.go b/store/sql/user/repo.go similarity index 100% rename from store/db/user/repo.go rename to store/sql/user/repo.go diff --git a/store/db/user/user_query.sql.go b/store/sql/user/user_query.sql.go similarity index 100% rename from store/db/user/user_query.sql.go rename to store/sql/user/user_query.sql.go diff --git a/store/db/v2/auth/auth.go b/store/v2/auth/auth.go similarity index 100% rename from store/db/v2/auth/auth.go rename to store/v2/auth/auth.go diff --git a/store/db/v2/user/user.go b/store/v2/user/user.go similarity index 100% rename from store/db/v2/user/user.go rename to store/v2/user/user.go From fcaaf7de4b917b36dcb374e1b1462629da16dbb6 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 21:30:12 +0100 Subject: [PATCH 147/246] jam service needs to get some tests --- service/jam/routes.go | 203 ------------- service/jam/service.go | 284 ++++++++++++++---- .../jam/{routes_test.go => service_test.go} | 0 3 files changed, 227 insertions(+), 260 deletions(-) delete mode 100644 service/jam/routes.go rename service/jam/{routes_test.go => service_test.go} (100%) diff --git a/service/jam/routes.go b/service/jam/routes.go deleted file mode 100644 index 1d1d762e..00000000 --- a/service/jam/routes.go +++ /dev/null @@ -1,203 +0,0 @@ -package jam - -import ( - "net/http" - - "context" - "encoding/json" - - "github.com/gorilla/websocket" - rmx "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/fp" - "github.com/rog-golang-buddies/rmx/internal/suid" - ws "github.com/rog-golang-buddies/rmx/internal/websocket" -) - -func (s *Service) handleCreateRoom() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.c.NewPool(4) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - s.l.Println("create a room", s.c.Size()) - - v := &session{ID: suid.FromUUID(uid)} - - s.respond(w, r, v, http.StatusOK) - } -} - -func (s *Service) handleGetRoom() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r) - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - // FIXME possible rename - // method as `Get` is nondescriptive - p, err := s.c.Get(uid) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - v := &session{ - ID: p.ID.ShortUUID(), - Users: fp.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), - } - - s.respond(w, r, v, http.StatusOK) - } -} - -func (s *Service) handleListRooms() http.HandlerFunc { - type response struct { - Sessions []session `json:"sessions"` - } - - return func(w http.ResponseWriter, r *http.Request) { - v := &response{ - Sessions: fp.FMap(s.c.List(), func(p *ws.Pool) session { - return session{ - ID: p.ID.ShortUUID(), - Users: fp.FMap( - p.Keys(), - func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }, - ), - UserCount: p.Size(), - } - }), - } - - s.respond(w, r, v, http.StatusOK) - } -} - -// Works with `chi.With` -func (s *Service) connectionPool(p *ws.Pool) func(f http.Handler) http.Handler { - return func(f http.Handler) http.Handler { - var fn func(w http.ResponseWriter, r *http.Request) - if p != nil { - fn = func(w http.ResponseWriter, r *http.Request) { - f.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), roomKey, p))) - } - } else { - fn = func(w http.ResponseWriter, r *http.Request) { - s.l.Println(r.URL.Path) - - uid, err := s.parseUUID(w, r) - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - p, err := s.c.Get(uid) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - r = r.WithContext(context.WithValue(r.Context(), roomKey, p)) - f.ServeHTTP(w, r) - } - } - - return http.HandlerFunc(fn) - } -} - -func (s *Service) upgradeHTTP(readBuf, writeBuf int) func(f http.Handler) http.Handler { - return func(f http.Handler) http.Handler { - u := &websocket.Upgrader{ - ReadBufferSize: readBuf, - WriteBufferSize: writeBuf, - CheckOrigin: func(r *http.Request) bool { return true }, - } - - fn := func(w http.ResponseWriter, r *http.Request) { - p, _ := r.Context().Value(roomKey).(*ws.Pool) - if p.Size() == p.MaxConn { - s.respond(w, r, ws.ErrMaxConn, http.StatusUnauthorized) - return - } - - c, err := p.NewConn(w, r, u) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - - r = r.WithContext(context.WithValue(r.Context(), upgradeKey, c)) - f.ServeHTTP(w, r) - } - - return http.HandlerFunc(fn) - } -} - -// converting to handler for middleware, in order to use Chi's default type - -func (s *Service) handleP2PComms() http.HandlerFunc { - // FIXME we will change this as I know this hasn't been - // was just my way of getting things working, not yet - // full agreement with this. - type response[T any] struct { - Typ rmx.MsgTyp `json:"type"` - Payload T `json:"payload"` - } - - type join struct { - ID suid.SUID `json:"id"` - SessionID suid.SUID `json:"sessionId"` - } - - type leave struct { - ID suid.SUID `json:"id"` - SessionID suid.SUID `json:"sessionId"` - Error any `json:"err"` - } - - return func(w http.ResponseWriter, r *http.Request) { - c := r.Context().Value(upgradeKey).(*ws.Conn) - - defer func() { - // FIXME send error when Leaving session pool - c.SendMessage(response[leave]{ - Typ: rmx.Leave, - Payload: leave{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, - }) - - c.Close() - }() - - if err := c.SendMessage(response[join]{ - Typ: rmx.Join, - Payload: join{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, - }); err != nil { - s.l.Println(err) - return - } - - // TODO could the API be adjusted such that - // this for-loop only needs to read and - // never touch the code for writing - for { - var msg response[json.RawMessage] - if err := c.ReadJSON(&msg); err != nil { - s.l.Println(err) - return - } - - // TODO here the message will be passed off to a different handler - // via a go routine* - if err := c.SendMessage(response[int]{Typ: rmx.Message, Payload: 10}); err != nil { - s.l.Println(err) - return - } - } - } -} diff --git a/service/jam/service.go b/service/jam/service.go index e8a81254..42ac53f5 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -1,102 +1,272 @@ package jam import ( + "context" + "encoding/json" "errors" "log" "net/http" "github.com/go-chi/chi/v5" + "github.com/gorilla/websocket" h "github.com/hyphengolang/prelude/http" + "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/suid" ws "github.com/rog-golang-buddies/rmx/internal/websocket" ) -type contextKey string +func (s *Service) routes() { + s.m.Route("/api/v1/jam", func(r chi.Router) { + r.Get("/", s.handleListRooms()) + r.Post("/", s.handleCreateRoom()) + r.Get("/{uuid}", s.handleGetRoom()) + }) -var ( - roomKey = contextKey("rmx-fetch-pool") - upgradeKey = contextKey("rmx-upgrade-http") -) + s.m.Route("/ws/jam", func(r chi.Router) { + r = r.With(s.connectionPool(nil), s.upgradeHTTP(1024, 1024)) + r.Get("/{uuid}", s.handleP2PComms()) + }) +} -func chain(hf http.HandlerFunc, mw ...h.MiddleWare) http.HandlerFunc { return h.Chain(hf, mw...) } +func (s *Service) handleP2PComms() http.HandlerFunc { + // FIXME we will change this as I know this hasn't been + // was just my way of getting things working, not yet + // full agreement with this. + type response[T any] struct { + Typ internal.MsgTyp `json:"type"` + Payload T `json:"payload"` + } -var ( - ErrNoCookie = errors.New("api: cookie not found") - ErrSessionNotFound = errors.New("api: session not found") - ErrSessionExists = errors.New("api: session already exists") -) + type join struct { + ID suid.SUID `json:"id"` + SessionID suid.SUID `json:"sessionId"` + } -type jam struct { - Name string `json:"name"` - BPM int `json:"bpm"` - ws.Pool + type leave struct { + ID suid.SUID `json:"id"` + SessionID suid.SUID `json:"sessionId"` + Error any `json:"err"` + } + + return func(w http.ResponseWriter, r *http.Request) { + c := r.Context().Value(internal.UpgradeKey).(*ws.Conn) + + defer func() { + // FIXME send error when Leaving session pool + c.SendMessage(response[leave]{ + Typ: internal.Leave, + Payload: leave{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, + }) + + c.Close() + }() + + if err := c.SendMessage(response[join]{ + Typ: internal.Join, + Payload: join{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, + }); err != nil { + s.log(err) + return + } + + // TODO could the API be adjusted such that + // this for-loop only needs to read and + // never touch the code for writing + for { + var msg response[json.RawMessage] + if err := c.ReadJSON(&msg); err != nil { + s.log(err) + return + } + + // TODO here the message will be passed off to a different handler + // via a go routine* + if err := c.SendMessage(response[int]{Typ: internal.Message, Payload: 10}); err != nil { + s.log(err) + return + } + } + } } -type session struct { - ID suid.SUID `json:"id"` - Name string `json:"name,omitempty"` - Users []suid.SUID `json:"users,omitempty"` - /* Not really required */ - UserCount int `json:"userCount"` +func (s *Service) handleCreateRoom() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + uid, err := s.c.NewPool(4) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + v := &session{ID: suid.FromUUID(uid)} + + s.respond(w, r, v, http.StatusOK) + } } -type User struct { - ID suid.SUID `json:"id"` - Name string `json:"name,omitempty"` - /* More fields can belong here */ +func (s *Service) handleGetRoom() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + uid, err := s.parseUUID(w, r) + if err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + // FIXME possible rename + // method as `Get` is nondescriptive + p, err := s.c.Get(uid) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + v := &session{ + ID: p.ID.ShortUUID(), + Users: fp.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), + } + + s.respond(w, r, v, http.StatusOK) + } } -type Service struct { - m chi.Router - l *log.Logger +func (s *Service) handleListRooms() http.HandlerFunc { + type response struct { + Sessions []session `json:"sessions"` + } - c *ws.Client + return func(w http.ResponseWriter, r *http.Request) { + v := &response{ + Sessions: fp.FMap(s.c.List(), func(p *ws.Pool) session { + return session{ + ID: p.ID.ShortUUID(), + Users: fp.FMap( + p.Keys(), + func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }, + ), + UserCount: p.Size(), + } + }), + } + + s.respond(w, r, v, http.StatusOK) + } } -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } +// needs to be moved into websocket package as its middleware +func (s *Service) connectionPool(p *ws.Pool) func(f http.Handler) http.Handler { + return func(f http.Handler) http.Handler { + var fn func(w http.ResponseWriter, r *http.Request) + if p != nil { + fn = func(w http.ResponseWriter, r *http.Request) { + f.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), internal.RoomKey, p))) + } + } else { + fn = func(w http.ResponseWriter, r *http.Request) { + uid, err := s.parseUUID(w, r) + if err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } -func NewService(r chi.Router) *Service { - s := &Service{r, log.Default(), ws.DefaultClient} - s.routes() - return s + p, err := s.c.Get(uid) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) + return + } + + r = r.WithContext(context.WithValue(r.Context(), internal.RoomKey, p)) + f.ServeHTTP(w, r) + } + } + + return http.HandlerFunc(fn) + } } -func DefaultService() *Service { - s := &Service{chi.NewMux(), log.Default(), ws.DefaultClient} - s.routes() - return s +// needs to be moved into websocket package as its middleware +func (s *Service) upgradeHTTP(readBuf, writeBuf int) func(f http.Handler) http.Handler { + return func(f http.Handler) http.Handler { + u := &websocket.Upgrader{ + ReadBufferSize: readBuf, + WriteBufferSize: writeBuf, + CheckOrigin: func(r *http.Request) bool { return true }, + } + + fn := func(w http.ResponseWriter, r *http.Request) { + p, _ := r.Context().Value(internal.RoomKey).(*ws.Pool) + if p.Size() == p.MaxConn { + s.respond(w, r, ws.ErrMaxConn, http.StatusUnauthorized) + return + } + + c, err := p.NewConn(w, r, u) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + return + } + + r = r.WithContext(context.WithValue(r.Context(), internal.UpgradeKey, c)) + f.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) + } } -func (s *Service) respond(w http.ResponseWriter, r *http.Request, data any, status int) { - h.Respond(w, r, data, status) +var ( + ErrNoCookie = errors.New("api: cookie not found") + ErrSessionNotFound = errors.New("api: session not found") + ErrSessionExists = errors.New("api: session already exists") +) + +type Service struct { + m chi.Router + c *ws.Client + + log func(s ...any) + logf func(string, ...any) + + respond func(http.ResponseWriter, *http.Request, any, int) + decode func(http.ResponseWriter, *http.Request, interface{}) error } -func (s *Service) decode(w http.ResponseWriter, r *http.Request, data interface{}) error { - return h.Decode(w, r, data) +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } + +func NewService(r chi.Router) *Service { + s := &Service{ + r, + ws.DefaultClient, + log.Print, + log.Printf, + h.Respond, + h.Decode, + } + s.routes() + return s } func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { return suid.ParseString(chi.URLParam(r, "uuid")) } -func (s *Service) routes() { - s.m.Route("/api/v1/jam", func(r chi.Router) { - r.Get("/", s.handleListRooms()) - r.Post("/", s.handleCreateRoom()) - r.Get("/{uuid}", s.handleGetRoom()) - - r.Get("/ping", s.handlePing) - }) +type jam struct { + Name string `json:"name"` + BPM int `json:"bpm"` + ws.Pool +} - // s.m.Get("/ws/jam/{uuid}", chain(s.handleP2PComms(), s.upgradeHTTP(1024, 1024), s.connectionPool(nil))) - s.m.Route("/ws/jam", func(r chi.Router) { - r = r.With(s.connectionPool(nil), s.upgradeHTTP(1024, 1024)) - r.Get("/{uuid}", s.handleP2PComms()) - }) +type session struct { + ID suid.SUID `json:"id"` + Name string `json:"name,omitempty"` + Users []suid.SUID `json:"users,omitempty"` + /* Not really required */ + UserCount int `json:"userCount"` } -func (s *Service) handlePing(w http.ResponseWriter, r *http.Request) { - s.respond(w, r, nil, http.StatusNoContent) +type User struct { + ID suid.SUID `json:"id"` + Name string `json:"name,omitempty"` + /* More fields can belong here */ } diff --git a/service/jam/routes_test.go b/service/jam/service_test.go similarity index 100% rename from service/jam/routes_test.go rename to service/jam/service_test.go From 4618ceb67a051d5d06146e685708e4bb62443d9e Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 21:31:12 +0100 Subject: [PATCH 148/246] moved auth into root internal package --- internal/auth/auth.go | 62 +--------- service/internal/auth/auth.go | 207 ---------------------------------- 2 files changed, 1 insertion(+), 268 deletions(-) delete mode 100644 service/internal/auth/auth.go diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 0bd31c90..837fc717 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -275,6 +275,7 @@ func Authentication(algo jwa.SignatureAlgorithm, key jwk.Key, cookieName ...stri } ctx := context.WithValue(r.Context(), internal.EmailKey, internal.Email(email)) + // ctx = context.WithValue(ctx, internal.EmailKey, r) r = r.WithContext(context.WithValue(ctx, internal.TokenKey, token)) h.ServeHTTP(w, r) } @@ -286,64 +287,3 @@ func Authentication(algo jwa.SignatureAlgorithm, key jwk.Key, cookieName ...stri return http.HandlerFunc(at) } } - -// Authenticate against the accessToken -func Authenticate(algo jwa.SignatureAlgorithm, key jwk.Key) func(http.Handler) http.Handler { - return func(h http.Handler) http.Handler { - f := func(w http.ResponseWriter, r *http.Request) { - // token, err := jwt.ParseRequest(r, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey(cookieName), jwt.WithKey(jwa.RS256, publicKey), jwt.WithValidate(true)) - token, err := jwt.ParseRequest(r, jwt.WithKey(algo, key)) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - email, ok := token.PrivateClaims()["email"].(string) - if !ok { - // NOTE unsure if we need to write anything more to the body - w.WriteHeader(http.StatusUnauthorized) - return - } - - // NOTE convert email from `string` type to `internal.Email` ? - r = r.WithContext(context.WithValue(r.Context(), internal.EmailKey, internal.Email(email))) - h.ServeHTTP(w, r) - } - - return http.HandlerFunc(f) - } -} - -// Authenticate against the refreshToken -func AuthenticateRefresh(algo jwa.SignatureAlgorithm, key jwk.Key, cookieName string) func(http.Handler) http.Handler { - return func(h http.Handler) http.Handler { - var refresh http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { - rc, err := r.Cookie(cookieName) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - token, err := jwt.Parse([]byte(rc.Value), jwt.WithKey(algo, key), jwt.WithValidate(true)) - - // token, err := jwt.ParseRequest(r, jwt.WithKey(algo, key), jwt.WithHeaderKey(cookieName)) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - email, ok := token.PrivateClaims()["email"].(string) - if !ok { - // NOTE unsure if we need to write anything more to the body - w.WriteHeader(http.StatusUnauthorized) - return - } - - r = r.WithContext(context.WithValue(r.Context(), internal.EmailKey, internal.Email(email))) - r = r.WithContext(context.WithValue(r.Context(), internal.TokenKey, token)) - h.ServeHTTP(w, r) - } - - return http.HandlerFunc(refresh) - } -} diff --git a/service/internal/auth/auth.go b/service/internal/auth/auth.go deleted file mode 100644 index bccb9a2d..00000000 --- a/service/internal/auth/auth.go +++ /dev/null @@ -1,207 +0,0 @@ -package auth - -import ( - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "time" - - "github.com/go-redis/redis/v9" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/pkg/errors" - "github.com/rog-golang-buddies/rmx/internal/fp" -) - -type Client struct { - rtdb, cidb *redis.Client -} - -var ( - ErrNotImplemented = errors.New("not implemented") - ErrGenerateKey = errors.New("failed to generate new ecdsa key pair") - ErrSignTokens = errors.New("failed to generate signed tokens") - ErrRTValidate = errors.New("failed to validate refresh token") -) - -func NewRedis(addr, password string) *Client { - rtdb := redis.Options{Addr: addr, Password: password, DB: 0} - cidb := redis.Options{Addr: addr, Password: password, DB: 1} - - c := &Client{redis.NewClient(&rtdb), redis.NewClient(&cidb)} - return c -} - -const ( - defaultAddr = "localhost:6379" - defaultPassword = "" -) - -var DefaultClient = &Client{ - rtdb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 0}), - cidb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 1}), -} - -func (c *Client) ValidateRefreshToken(ctx context.Context, token string) error { - tc, err := ParseRefreshTokenClaims(token) - if err != nil { - return err - } - email, ok := tc.PrivateClaims()["email"].(string) - if !ok { - return ErrRTValidate - } - - cid := tc.Subject() - if err := c.ValidateClientID(ctx, cid); err != nil { - return err - } - - if _, err := c.rtdb.Get(ctx, token).Result(); err != nil { - switch err { - case redis.Nil: - return nil - default: - return err - } - } - - err = c.RevokeClientID(ctx, cid, email) - if err != nil { - return err - } - - return ErrRTValidate -} - -func (c *Client) RevokeClientID(ctx context.Context, cid, email string) error { - _, err := c.cidb.Set(ctx, cid, email, RefreshTokenExpiry).Result() - if err != nil { - return err - } - return nil -} - -func (c *Client) RevokeRefreshToken(ctx context.Context, token string) error { - _, err := c.rtdb.Set(ctx, token, nil, RefreshTokenExpiry).Result() - return err -} - -func (c *Client) ValidateClientID(ctx context.Context, cid string) error { - // check if a key with client id exists - // if the key exists it means that the client id is revoked and token should be denied - // we don't need the email value here - _, err := c.cidb.Get(ctx, cid).Result() - if err != nil { - switch err { - case redis.Nil: - return nil - default: - return ErrRTValidate - } - } - - return ErrRTValidate -} - -/* -// would like to find an alternative to using `os` package -func LoadPEM(path string) (private, public jwk.Key, err error) { - buf, err := os.ReadFile(path) - if err != nil { - return - } - - return GenerateKeys(string(buf)) -} -*/ - -func GenerateKeys() (jwk.Key, jwk.Key, error) { - private, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, nil, err - } - - key, err := jwk.FromRaw(private) - if err != nil { - return nil, nil, err - } - - _, ok := key.(jwk.ECDSAPrivateKey) - if !ok { - return nil, nil, ErrGenerateKey - } - - pub, err := key.PublicKey() - if err != nil { - return nil, nil, err - } - - return key, pub, nil -} - -func SignToken(key *jwk.Key, opt *TokenOption) ([]byte, error) { - var t time.Time - if opt.IssuedAt.IsZero() { - t = time.Now().UTC() - } else { - t = opt.IssuedAt - } - - token, err := jwt.NewBuilder(). - Issuer(opt.Issuer). - Audience(opt.Audience). - Subject(opt.Subject). - IssuedAt(t). - Expiration(t.Add(opt.Expiration)). - Build() - if err != nil { - return nil, ErrSignTokens - } - - for _, c := range opt.Claims { - if !c.HasValue() { - return nil, fp.ErrTuple - } - - err := token.Set(c[0], c[1]) - if err != nil { - return nil, ErrSignTokens - } - } - - return jwt.Sign(token, jwt.WithKey(jwa.ES256, key)) -} - -func ParseRefreshTokenClaims(token string) (jwt.Token, error) { return jwt.Parse([]byte(token)) } - -func ParseRefreshTokenWithValidate(key *jwk.Key, token string) (jwt.Token, error) { - payload, err := jwt.Parse([]byte(token), - jwt.WithKey(jwa.ES256, key), - jwt.WithValidate(true)) - if err != nil { - return nil, err - } - - return payload, nil -} - -type TokenOption struct { - Issuer string - Audience []string - Subject string - Claims []fp.Tuple - IssuedAt time.Time - Expiration time.Duration -} - -type authCtxKey string - -const ( - RefreshTokenCookieName = "RMX_REFRESH_TOKEN" - RefreshTokenExpiry = time.Hour * 24 * 7 - AccessTokenExpiry = time.Minute * 5 - EmailKey = authCtxKey("rmx-email") -) From 136b05ab6f6e72cbec395104c12836c8487d6e49 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 21:31:46 +0100 Subject: [PATCH 149/246] clean up --- internal/internal.go | 10 ++++++---- service/auth/service_test.go | 2 +- service/service.go | 8 +++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/internal.go b/internal/internal.go index 990fd265..b7b91811 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -6,7 +6,6 @@ import ( "net/mail" "strings" - "github.com/lestrrat-go/jwx/v2/jwt" "github.com/rog-golang-buddies/rmx/internal/suid" gpv "github.com/wagslane/go-password-validator" "golang.org/x/crypto/bcrypt" @@ -24,8 +23,10 @@ var ( type ContextKey string const ( - EmailKey = ContextKey("email-key") - TokenKey = ContextKey("token-key") + EmailKey = ContextKey("account-email") + TokenKey = ContextKey("jwt-token-key") + RoomKey = ContextKey("conn-pool-key") + UpgradeKey = ContextKey("upgrade-http-key") ) type MsgTyp int @@ -103,7 +104,8 @@ type TokenClient interface { } type RTokenClient interface { - Validate(ctx context.Context, token jwt.Token) error + // Validate(ctx context.Context, token jwt.Token) error + ValidateRefreshToken(ctx context.Context, token string) error } type WTokenClient interface{} diff --git a/service/auth/service_test.go b/service/auth/service_test.go index b680bc3d..cafdde63 100644 --- a/service/auth/service_test.go +++ b/service/auth/service_test.go @@ -11,7 +11,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/rog-golang-buddies/rmx/internal/is" - "github.com/rog-golang-buddies/rmx/store/db/v2/user" + "github.com/rog-golang-buddies/rmx/store/v2/user" ) const applicationJson = "application/json" diff --git a/service/service.go b/service/service.go index 6760e8e7..eaeb1c25 100644 --- a/service/service.go +++ b/service/service.go @@ -16,16 +16,14 @@ func (s *Service) routes() { type Service struct { m chi.Router - l *log.Logger + log func(s ...any) + logf func(string, ...any) } func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } func New(st store.Store) *Service { - s := &Service{chi.NewMux(), log.Default()} + s := &Service{chi.NewMux(), log.Print, log.Printf} s.routes() - - // jam.NewService(s.m) - // user.NewService(s.m, ur) return s } From 660e2c415c5dbf081ffcb1f2883819d87c7c06dd Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 21:32:12 +0100 Subject: [PATCH 150/246] cmd main needs to held off till other packages have good tests --- cmd/main.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 168a5990..15d929d2 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -12,8 +12,6 @@ import ( "github.com/rs/cors" "golang.org/x/sync/errgroup" - - "github.com/rog-golang-buddies/rmx/service" ) // var ( @@ -102,7 +100,7 @@ func run(cfg *Config) error { srv := http.Server{ Addr: ":" + strconv.Itoa(cfg.Port), - Handler: cors.New(c).Handler(service.Default()), + Handler: cors.New(c).Handler(http.NotFoundHandler()), // max time to read request from the client ReadTimeout: 10 * time.Second, // max time to write response to the client From 31fecd3e664f48703890064a20b352e3acb8041b Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 21:36:06 +0100 Subject: [PATCH 151/246] auth/ and user/ packages now root of store/ package --- store/{v2 => }/auth/auth.go | 0 store/{v2 => }/user/user.go | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename store/{v2 => }/auth/auth.go (100%) rename store/{v2 => }/user/user.go (100%) diff --git a/store/v2/auth/auth.go b/store/auth/auth.go similarity index 100% rename from store/v2/auth/auth.go rename to store/auth/auth.go diff --git a/store/v2/user/user.go b/store/user/user.go similarity index 100% rename from store/v2/user/user.go rename to store/user/user.go From 518c7e9fa25bbe4ea80d8bcbf7e466b676980fd6 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 21:36:33 +0100 Subject: [PATCH 152/246] dto not needed --- internal/dto/dto.go | 120 -------------------------------------------- 1 file changed, 120 deletions(-) delete mode 100644 internal/dto/dto.go diff --git a/internal/dto/dto.go b/internal/dto/dto.go deleted file mode 100644 index 9846504d..00000000 --- a/internal/dto/dto.go +++ /dev/null @@ -1,120 +0,0 @@ -package dto - -import ( - "context" - "errors" - "net/mail" - - "github.com/rog-golang-buddies/rmx/internal/suid" - gpv "github.com/wagslane/go-password-validator" - "golang.org/x/crypto/bcrypt" -) - -var ErrInvalidEmail = errors.New("rmx: invalid email") -var ErrNotImplemented = errors.New("rmx: not yet implemented") - -type JamRepo interface { -} - -type User struct { - ID suid.UUID `json:"id"` - Username string `json:"username"` - Email Email `json:"email"` - Password Password `json:"-"` -} - -func (u *User) HashPassword() error { - h, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost) - if err != nil { - return err - } - u.Password = Password(h) - return nil -} - -func (u *User) ComparePassword(p string) error { - return bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(p)) -} - -type UserRepo interface { - RUserRepo - WUserRepo -} - -type RUserRepo interface { - // Lookup(uid *suid.UUID) (User, error) - // LookupEmail(email string) (User, error) - // ListAll() ([]User, error) -} - -type WUserRepo interface { - Insert(ctx context.Context, u *User) error - // Remove(uid *suid.UUID) error -} - -/* -var emailRegex = regexp.MustCompile( - `^[a-zA-Z0-9.!#$%&’*+/=?^_\x60{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$`, -) -*/ - -type Email string - -func (e *Email) String() string { return string(*e) } - -func (e *Email) Validate() error { - _, err := mail.ParseAddress(e.String()) - if err != nil { - return err - } - return nil -} - -/* -func (e *Email) UnmarshalJSON(b []byte) error { - if s := b[1 : len(b)-1]; emailRegex.Match(s) { - *e = Email(s) - return nil - } - - return ErrInvalidEmail -} -*/ - -// NOTE: better change to something in range 50-70 -const minEntropy float64 = 10.0 - -type Password string - -func (p *Password) String() string { return string(*p) } - -func (p *Password) Validate() error { - return gpv.Validate(p.String(), minEntropy) -} - -/* -func (p *Password) UnmarshalJSON(b []byte) error { - *p = Password(b[1 : len(b)-1]) - return gpv.Validate(string(*p), minEntropy) -} - -func (p *Password) MarshalJSON() (b []byte, err error) { - var sb strings.Builder - sb.WriteRune('"') - sb.WriteString(string(*p)) - sb.WriteRune('"') - return []byte(sb.String()), nil -} -*/ - -func (p *Password) Hash() (PasswordHash, error) { return newPasswordHash(*p) } - -type PasswordHash []byte - -func newPasswordHash(pw Password) (PasswordHash, error) { - return bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) -} - -func (pw *PasswordHash) Compare(cmp Password) error { - return bcrypt.CompareHashAndPassword(*pw, []byte(cmp)) -} From da9393edfec8b91c202d75c90e195f6f8a627233 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 21:37:54 +0100 Subject: [PATCH 153/246] deleted unneeded pacakges --- internal/websocket/client.go | 4 +- service/auth/user/service.go | 73 ++++++++++++++++++++++++++++++++ test/mock/repo.go | 80 ------------------------------------ 3 files changed, 75 insertions(+), 82 deletions(-) create mode 100644 service/auth/user/service.go delete mode 100644 test/mock/repo.go diff --git a/internal/websocket/client.go b/internal/websocket/client.go index 984b8554..1d09b680 100644 --- a/internal/websocket/client.go +++ b/internal/websocket/client.go @@ -4,7 +4,7 @@ import ( "log" "sync" - "github.com/rog-golang-buddies/rmx/internal/dto" + "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/suid" "golang.org/x/exp/maps" ) @@ -30,7 +30,7 @@ func NewClient() *Client { func (c *Client) Size() int { return len(c.ps) } func (c *Client) Close() error { - return dto.ErrNotImplemented + return internal.ErrNotImplemented } func (c *Client) NewPool(maxCount int) (suid.UUID, error) { diff --git a/service/auth/user/service.go b/service/auth/user/service.go new file mode 100644 index 00000000..478552fc --- /dev/null +++ b/service/auth/user/service.go @@ -0,0 +1,73 @@ +package user + +import ( + "context" + "errors" + "log" + "net/http" + + "github.com/go-chi/chi/v5" + + h "github.com/hyphengolang/prelude/http" + + "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/suid" + // big no-no +) + +var ( + ErrNoCookie = errors.New("user: cookie not found") + ErrSessionNotFound = errors.New("user: session not found") + ErrSessionExists = errors.New("user: session already exists") +) + +// source: https://stackoverflow.com/questions/7140074/restfully-design-login-or-register-resources +// +// [+] GET /user/me - get my user details +// [-] GET /user/{uuid} - get user info +// [-] GET /user - get all users +// [+] POST /user - register new user +// [ ] POST /user/{uuid} - update information about user +// [ ] DELETE /user - delete user from database +// [+] GET /session - refresh session token +// [+] POST /session - create session (due to logging in) +// [+] DELETE /session - delete session (due to logging out) +func (s *Service) routes() {} + +type Service struct { + ctx context.Context + + m chi.Router + + log func(...any) + logf func(string, ...any) + + decode func(http.ResponseWriter, *http.Request, any) error + respond func(http.ResponseWriter, *http.Request, any, int) + created func(http.ResponseWriter, *http.Request, string) + setCookie func(http.ResponseWriter, *http.Cookie) +} + +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } + +func NewService(ctx context.Context, m chi.Router, r internal.UserRepo) *Service { + s := &Service{ + ctx, + m, + + log.Println, + log.Printf, + + h.Decode, + h.Respond, + h.Created, + http.SetCookie, + } + + s.routes() + return s +} + +func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { + return suid.ParseString(chi.URLParam(r, "uuid")) +} diff --git a/test/mock/repo.go b/test/mock/repo.go deleted file mode 100644 index e4c1f511..00000000 --- a/test/mock/repo.go +++ /dev/null @@ -1,80 +0,0 @@ -package mock - -import ( - "errors" - "sync" - - "github.com/rog-golang-buddies/rmx/internal/dto" - "github.com/rog-golang-buddies/rmx/internal/suid" - "golang.org/x/exp/maps" -) - -var ( - errTodo = errors.New("not yet implemented") - errNotFound = errors.New("user not found") - errExists = errors.New("user already exists") -) - -type userRepo struct { - mu sync.Mutex - us map[suid.UUID]dto.User -} - -func UserRepo() *userRepo { - r := &userRepo{ - us: map[suid.UUID]dto.User{}, - } - return r -} - -func (r *userRepo) ListAll() ([]dto.User, error) { - r.mu.Lock() - defer r.mu.Unlock() - - return maps.Values(r.us), nil -} - -func (r *userRepo) Lookup(uid *suid.UUID) (dto.User, error) { - r.mu.Lock() - defer r.mu.Unlock() - - for _, u := range r.us { - if &u.ID == uid { - return u, nil - } - } - - return dto.User{}, errNotFound -} - -func (r *userRepo) LookupEmail(email string) (dto.User, error) { - r.mu.Lock() - defer r.mu.Unlock() - - for _, u := range r.us { - if string(u.Email) == email { - return u, nil - } - } - - return dto.User{}, errNotFound -} - -func (r *userRepo) Add(u *dto.User) error { - r.mu.Lock() - defer r.mu.Unlock() - - for _, v := range r.us { - if v.Username == u.Username { - return errExists - } - } - - r.us[u.ID] = *u - - return nil -} - -func (r *userRepo) Remove(uid *suid.UUID) error { - return nil -} From 999d50c5f0e3f24ca9cc3029dc60f2d734da9896 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 21:38:56 +0100 Subject: [PATCH 154/246] not needed --- service/auth/user/service.go | 73 ------------------------------------ 1 file changed, 73 deletions(-) delete mode 100644 service/auth/user/service.go diff --git a/service/auth/user/service.go b/service/auth/user/service.go deleted file mode 100644 index 478552fc..00000000 --- a/service/auth/user/service.go +++ /dev/null @@ -1,73 +0,0 @@ -package user - -import ( - "context" - "errors" - "log" - "net/http" - - "github.com/go-chi/chi/v5" - - h "github.com/hyphengolang/prelude/http" - - "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/suid" - // big no-no -) - -var ( - ErrNoCookie = errors.New("user: cookie not found") - ErrSessionNotFound = errors.New("user: session not found") - ErrSessionExists = errors.New("user: session already exists") -) - -// source: https://stackoverflow.com/questions/7140074/restfully-design-login-or-register-resources -// -// [+] GET /user/me - get my user details -// [-] GET /user/{uuid} - get user info -// [-] GET /user - get all users -// [+] POST /user - register new user -// [ ] POST /user/{uuid} - update information about user -// [ ] DELETE /user - delete user from database -// [+] GET /session - refresh session token -// [+] POST /session - create session (due to logging in) -// [+] DELETE /session - delete session (due to logging out) -func (s *Service) routes() {} - -type Service struct { - ctx context.Context - - m chi.Router - - log func(...any) - logf func(string, ...any) - - decode func(http.ResponseWriter, *http.Request, any) error - respond func(http.ResponseWriter, *http.Request, any, int) - created func(http.ResponseWriter, *http.Request, string) - setCookie func(http.ResponseWriter, *http.Cookie) -} - -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } - -func NewService(ctx context.Context, m chi.Router, r internal.UserRepo) *Service { - s := &Service{ - ctx, - m, - - log.Println, - log.Printf, - - h.Decode, - h.Respond, - h.Created, - http.SetCookie, - } - - s.routes() - return s -} - -func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { - return suid.ParseString(chi.URLParam(r, "uuid")) -} From fb67e02f846db7623bd8146d565a993315669f6d Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 21:45:16 +0100 Subject: [PATCH 155/246] service test still works after changing inmem repo --- service/auth/service_test.go | 2 +- store/user/user.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/service/auth/service_test.go b/service/auth/service_test.go index cafdde63..746bade8 100644 --- a/service/auth/service_test.go +++ b/service/auth/service_test.go @@ -11,7 +11,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/rog-golang-buddies/rmx/internal/is" - "github.com/rog-golang-buddies/rmx/store/v2/user" + "github.com/rog-golang-buddies/rmx/store/user" ) const applicationJson = "application/json" diff --git a/store/user/user.go b/store/user/user.go index 3cfe4e6a..bf63baec 100644 --- a/store/user/user.go +++ b/store/user/user.go @@ -13,7 +13,7 @@ import ( type User struct { ID suid.UUID Username string - Email string + Email internal.Email Password internal.PasswordHash CreatedAt time.Time } @@ -37,7 +37,7 @@ func (r *repo) Insert(ctx context.Context, iu *internal.User) error { return internal.ErrAlreadyExists } - u := &User{iu.ID, iu.Username, iu.Email.String(), iu.Password, time.Now()} + u := &User{iu.ID, iu.Username, iu.Email, iu.Password, time.Now()} r.mei[iu.Email.String()], r.miu[iu.ID] = u, u return nil From 9f3afec8a3c5d9aeef0f27e6d9b8be92bfc64290 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 21:50:07 +0100 Subject: [PATCH 156/246] token client cleanup --- internal/internal.go | 2 +- service/auth/service.go | 11 +++++------ service/auth/service_test.go | 3 ++- store/auth/auth.go | 2 +- store/user/user.go | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/internal.go b/internal/internal.go index b7b91811..87c24981 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -105,7 +105,7 @@ type TokenClient interface { type RTokenClient interface { // Validate(ctx context.Context, token jwt.Token) error - ValidateRefreshToken(ctx context.Context, token string) error + Validate(ctx context.Context, token string) error } type WTokenClient interface{} diff --git a/service/auth/service.go b/service/auth/service.go index 46fca512..8753b05d 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -84,7 +84,7 @@ func (s *Service) handleRefresh(key jwk.Key) http.HandlerFunc { // to come up with a cleaner solution k, _ := r.Cookie(cookieName) - err := s.t.ValidateRefreshToken(r.Context(), k.Value) + err := s.tc.Validate(r.Context(), k.Value) if err != nil { s.respond(w, r, err, http.StatusTeapot) return @@ -219,8 +219,8 @@ type Service struct { m chi.Router - r internal.UserRepo - t internal.TokenClient + r internal.UserRepo + tc internal.TokenClient log func(...any) logf func(string, ...any) @@ -286,15 +286,14 @@ func (s *Service) signedTokens(key jwk.Key, email string, uuid suid.UUID) (its, func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func NewService(ctx context.Context, m chi.Router, r internal.UserRepo) *Service { - var t = auth.DefaultClient +func NewService(ctx context.Context, m chi.Router, r internal.UserRepo, tc internal.TokenClient) *Service { s := &Service{ ctx, m, r, - t, + tc, log.Println, log.Printf, diff --git a/service/auth/service_test.go b/service/auth/service_test.go index 746bade8..573704df 100644 --- a/service/auth/service_test.go +++ b/service/auth/service_test.go @@ -11,6 +11,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/rog-golang-buddies/rmx/internal/is" + "github.com/rog-golang-buddies/rmx/store/auth" "github.com/rog-golang-buddies/rmx/store/user" ) @@ -19,7 +20,7 @@ const applicationJson = "application/json" var s http.Handler func init() { - s = NewService(context.Background(), chi.NewMux(), user.MapRepo) + s = NewService(context.Background(), chi.NewMux(), user.DefaultRepo, auth.DefaultClient) } func TestService(t *testing.T) { diff --git a/store/auth/auth.go b/store/auth/auth.go index c6009758..524dd6f1 100644 --- a/store/auth/auth.go +++ b/store/auth/auth.go @@ -38,7 +38,7 @@ var DefaultClient = &Client{ cidb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 1}), } -func (c *Client) ValidateRefreshToken(ctx context.Context, token string) error { +func (c *Client) Validate(ctx context.Context, token string) error { tc, err := ParseRefreshTokenClaims(token) if err != nil { return err diff --git a/store/user/user.go b/store/user/user.go index bf63baec..677b8ff6 100644 --- a/store/user/user.go +++ b/store/user/user.go @@ -18,7 +18,7 @@ type User struct { CreatedAt time.Time } -var MapRepo = &repo{miu: make(map[suid.UUID]*User), mei: make(map[string]*User), log: log.Println, logf: log.Printf} +var DefaultRepo = &repo{miu: make(map[suid.UUID]*User), mei: make(map[string]*User), log: log.Println, logf: log.Printf} type repo struct { mu sync.Mutex From 9ce634e822797ee974fffd6605b45d131ee0ce83 Mon Sep 17 00:00:00 2001 From: adoublef Date: Thu, 6 Oct 2022 21:59:45 +0100 Subject: [PATCH 157/246] add in-mem client --- store/auth/auth.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/store/auth/auth.go b/store/auth/auth.go index 524dd6f1..b6b662ee 100644 --- a/store/auth/auth.go +++ b/store/auth/auth.go @@ -9,6 +9,10 @@ import ( "github.com/lestrrat-go/jwx/v2/jwt" ) +type client struct { + rt, ci *redis.Client +} + type Client struct { rtdb, cidb *redis.Client } @@ -73,10 +77,7 @@ func (c *Client) Validate(ctx context.Context, token string) error { func (c *Client) RevokeClientID(ctx context.Context, cid, email string) error { _, err := c.cidb.Set(ctx, cid, email, RefreshTokenExpiry).Result() - if err != nil { - return err - } - return nil + return err } func (c *Client) RevokeRefreshToken(ctx context.Context, token string) error { From 3ea6553e449ea737688d3ab94b7c38ff1df1783c Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 7 Oct 2022 00:30:02 +0330 Subject: [PATCH 158/246] add fatal to use instead of panic --- service/service.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/service/service.go b/service/service.go index eaeb1c25..76fe8723 100644 --- a/service/service.go +++ b/service/service.go @@ -16,14 +16,16 @@ func (s *Service) routes() { type Service struct { m chi.Router - log func(s ...any) - logf func(string, ...any) + log func(s ...any) + logf func(string, ...any) + fatal func(s ...any) + fatalf func(string, ...any) } func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } func New(st store.Store) *Service { - s := &Service{chi.NewMux(), log.Print, log.Printf} + s := &Service{chi.NewMux(), log.Print, log.Printf, log.Fatal, log.Fatalf} s.routes() return s } From 7550601d28e5e9b1b4abf49c1c5a628064979655 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 7 Oct 2022 01:01:19 +0330 Subject: [PATCH 159/246] change Authentication to ParseAuth --- internal/auth/auth.go | 16 +++++++++++++--- internal/auth/auth_test.go | 10 +++++++--- service/auth/service.go | 19 +++++++++++++++---- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 837fc717..b0c63af8 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -233,7 +233,11 @@ const ( /* */ -func Authentication(algo jwa.SignatureAlgorithm, key jwk.Key, cookieName ...string) func(http.Handler) http.Handler { +func ParseAuth( + algo jwa.SignatureAlgorithm, + key jwk.Key, + cookieName ...string, +) func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { var at http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { token, err := jwt.ParseRequest(r, jwt.WithKey(algo, key)) @@ -250,7 +254,9 @@ func Authentication(algo jwa.SignatureAlgorithm, key jwk.Key, cookieName ...stri } // NOTE convert email from `string` type to `internal.Email` ? - r = r.WithContext(context.WithValue(r.Context(), internal.EmailKey, internal.Email(email))) + r = r.WithContext( + context.WithValue(r.Context(), internal.EmailKey, internal.Email(email)), + ) h.ServeHTTP(w, r) } @@ -261,7 +267,11 @@ func Authentication(algo jwa.SignatureAlgorithm, key jwk.Key, cookieName ...stri return } - token, err := jwt.Parse([]byte(rc.Value), jwt.WithKey(algo, key), jwt.WithValidate(true)) + token, err := jwt.Parse( + []byte(rc.Value), + jwt.WithKey(algo, key), + jwt.WithValidate(true), + ) if err != nil { w.WriteHeader(http.StatusUnauthorized) return diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index c66eb86f..2f0b7048 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -72,7 +72,7 @@ func TestMiddleware(t *testing.T) { ats, err := SignToken(key.Private(), &opt) is.NoErr(err) // signing access token - h := Authentication(opt.Algo, key.Public())(http.NotFoundHandler()) + h := ParseAuth(opt.Algo, key.Public())(http.NotFoundHandler()) req, _ := http.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(ats))) @@ -100,7 +100,7 @@ func TestMiddleware(t *testing.T) { rts, err := SignToken(key.Private(), &opt) is.NoErr(err) // signing refresh token - h := Authentication(opt.Algo, key.Public(), cookieName)(http.NotFoundHandler()) + h := ParseAuth(opt.Algo, key.Public(), cookieName)(http.NotFoundHandler()) req, _ := http.NewRequest(http.MethodGet, "/", nil) req.AddCookie(&http.Cookie{ @@ -146,7 +146,11 @@ func TestMiddleware(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, "/", nil) req.AddCookie(c) - _, err = jwt.Parse([]byte(c.Value), jwt.WithKey(opt.Algo, key.Public()), jwt.WithValidate(true)) + _, err = jwt.Parse( + []byte(c.Value), + jwt.WithKey(opt.Algo, key.Public()), + jwt.WithValidate(true), + ) is.NoErr(err) // parsing jwk page not found }) } diff --git a/service/auth/service.go b/service/auth/service.go index 8753b05d..7bdce071 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -57,12 +57,14 @@ func (s *Service) routes() { r.Delete("/sign-out", s.handleSignOut()) r.Post("/sign-up", s.handleSignUp()) - auth := r.With(auth.Authentication(jwa.ES256, key.Public(), cookieName)) // passing cookie is required + auth := r.With( + auth.ParseAuth(jwa.ES256, key.Public(), cookieName), + ) // passing cookie is required auth.Get("/refresh", s.handleRefresh(key.Private())) }) s.m.Route("/api/v2/account", func(r chi.Router) { - auth := r.With(auth.Authentication(jwa.ES256, key.Public())) + auth := r.With(auth.ParseAuth(jwa.ES256, key.Public())) auth.Get("/me", s.handleIdentity()) }) } @@ -258,7 +260,11 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, } // TODO there is two cid's being used here, need clarification -func (s *Service) signedTokens(key jwk.Key, email string, uuid suid.UUID) (its, ats, rts []byte, err error) { +func (s *Service) signedTokens( + key jwk.Key, + email string, + uuid suid.UUID, +) (its, ats, rts []byte, err error) { opt := auth.TokenOption{ Issuer: "github.com/rog-golang-buddies/rmx", Subject: uuid.String(), // new client ID for tracking user connections @@ -286,7 +292,12 @@ func (s *Service) signedTokens(key jwk.Key, email string, uuid suid.UUID) (its, func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func NewService(ctx context.Context, m chi.Router, r internal.UserRepo, tc internal.TokenClient) *Service { +func NewService( + ctx context.Context, + m chi.Router, + r internal.UserRepo, + tc internal.TokenClient, +) *Service { s := &Service{ ctx, From 6f810bc6738ba7a9ee7e7dc5c76b76242ad1d144 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 7 Oct 2022 01:33:24 +0330 Subject: [PATCH 160/246] fix sql and redis interfaces --- internal/auth/auth.go | 6 +++--- internal/internal.go | 17 ++++++++++------- service/auth/service.go | 2 +- service/auth/service_test.go | 9 ++++++--- store/auth/auth.go | 15 +++++++++++++++ store/sql/user/repo.go | 2 +- store/user/user.go | 27 ++++++++++++++++++++++++++- 7 files changed, 62 insertions(+), 16 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index b0c63af8..df275a98 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -72,7 +72,7 @@ func (c *Client) ValidateRefreshToken(ctx context.Context, token string) error { } } - err = c.RevokeClientID(ctx, cid, email) + err = c.BlackListClientID(ctx, cid, email) if err != nil { return err } @@ -80,7 +80,7 @@ func (c *Client) ValidateRefreshToken(ctx context.Context, token string) error { return ErrRTValidate } -func (c *Client) RevokeClientID(ctx context.Context, cid, email string) error { +func (c *Client) BlackListClientID(ctx context.Context, cid, email string) error { _, err := c.cidb.Set(ctx, cid, email, RefreshTokenExpiry).Result() if err != nil { return err @@ -88,7 +88,7 @@ func (c *Client) RevokeClientID(ctx context.Context, cid, email string) error { return nil } -func (c *Client) RevokeRefreshToken(ctx context.Context, token string) error { +func (c *Client) BlackListRefreshToken(ctx context.Context, token string) error { _, err := c.rtdb.Set(ctx, token, nil, RefreshTokenExpiry).Result() return err } diff --git a/internal/internal.go b/internal/internal.go index 87c24981..241f21dc 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -104,11 +104,14 @@ type TokenClient interface { } type RTokenClient interface { - // Validate(ctx context.Context, token jwt.Token) error - Validate(ctx context.Context, token string) error + ValidateRefreshToken(ctx context.Context, token string) error + ValidateClientID(ctx context.Context, cid string) error } -type WTokenClient interface{} +type WTokenClient interface { + BlackListClientID(ctx context.Context, cid, email string) error + BlackListRefreshToken(ctx context.Context, token string) error +} type UserRepo interface { RUserRepo @@ -116,15 +119,15 @@ type UserRepo interface { } type RUserRepo interface { - // Lookup(uid *suid.UUID) (User, error) - // LookupEmail(email string) (User, error) - // ListAll() ([]User, error) + Lookup(uid *suid.UUID) (User, error) + LookupEmail(email string) (User, error) + ListAll() ([]User, error) Select(ctx context.Context, key any) (*User, error) } type WUserRepo interface { Insert(ctx context.Context, u *User) error - // Remove(uid *suid.UUID) error + Remove(uid *suid.UUID) error } // Custom user type required diff --git a/service/auth/service.go b/service/auth/service.go index 7bdce071..8a3cd8aa 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -86,7 +86,7 @@ func (s *Service) handleRefresh(key jwk.Key) http.HandlerFunc { // to come up with a cleaner solution k, _ := r.Cookie(cookieName) - err := s.tc.Validate(r.Context(), k.Value) + err := s.tc.ValidateRefreshToken(r.Context(), k.Value) if err != nil { s.respond(w, r, err, http.StatusTeapot) return diff --git a/service/auth/service_test.go b/service/auth/service_test.go index 573704df..ee9f47cb 100644 --- a/service/auth/service_test.go +++ b/service/auth/service_test.go @@ -38,7 +38,8 @@ func TestService(t *testing.T) { "password":"fizz_$PW_10" }` - res, _ := srv.Client().Post(srv.URL+"/api/v2/auth/sign-up", applicationJson, strings.NewReader(payload)) + res, _ := srv.Client(). + Post(srv.URL+"/api/v2/auth/sign-up", applicationJson, strings.NewReader(payload)) is.Equal(res.StatusCode, http.StatusCreated) }) @@ -49,7 +50,8 @@ func TestService(t *testing.T) { "password":"fizz_$PW_10" }` - res, _ := srv.Client().Post(srv.URL+"/api/v2/auth/sign-in", applicationJson, strings.NewReader(payload)) + res, _ := srv.Client(). + Post(srv.URL+"/api/v2/auth/sign-in", applicationJson, strings.NewReader(payload)) is.Equal(res.StatusCode, http.StatusOK) type body struct { @@ -80,7 +82,8 @@ func TestService(t *testing.T) { "password":"fizz_$PW_10" }` - res, _ := srv.Client().Post(srv.URL+"/api/v2/auth/sign-in", applicationJson, strings.NewReader(payload)) + res, _ := srv.Client(). + Post(srv.URL+"/api/v2/auth/sign-in", applicationJson, strings.NewReader(payload)) is.Equal(res.StatusCode, http.StatusOK) // add refresh token // get the refresh token from the response's `Set-Cookie` header diff --git a/store/auth/auth.go b/store/auth/auth.go index 524dd6f1..6450cc33 100644 --- a/store/auth/auth.go +++ b/store/auth/auth.go @@ -13,6 +13,21 @@ type Client struct { rtdb, cidb *redis.Client } +// ValidateRefreshToken implements internal.TokenClient +func (*Client) ValidateRefreshToken(ctx context.Context, token string) error { + panic("unimplemented") +} + +// BlackListClientID implements internal.TokenClient +func (*Client) BlackListClientID(ctx context.Context, cid string, email string) error { + panic("unimplemented") +} + +// BlackListRefreshToken implements internal.TokenClient +func (*Client) BlackListRefreshToken(ctx context.Context, token string) error { + panic("unimplemented") +} + var ( ErrNotImplemented = errors.New("not implemented") ErrGenerateKey = errors.New("failed to generate new ecdsa key pair") diff --git a/store/sql/user/repo.go b/store/sql/user/repo.go index ec2c1240..409457e5 100644 --- a/store/sql/user/repo.go +++ b/store/sql/user/repo.go @@ -43,7 +43,7 @@ func (r *userRepo) LookupEmail(email string) (User, error) { return q.GetUserByEmail(ctx, email) } -func (r *userRepo) Add(u *CreateUserParams) error { +func (r *userRepo) Insert(u *CreateUserParams) error { ctx := context.Background() q := New(r.DBConn) _, err := q.CreateUser(ctx, u) diff --git a/store/user/user.go b/store/user/user.go index 677b8ff6..594b3a7f 100644 --- a/store/user/user.go +++ b/store/user/user.go @@ -18,7 +18,12 @@ type User struct { CreatedAt time.Time } -var DefaultRepo = &repo{miu: make(map[suid.UUID]*User), mei: make(map[string]*User), log: log.Println, logf: log.Printf} +var DefaultRepo = &repo{ + miu: make(map[suid.UUID]*User), + mei: make(map[string]*User), + log: log.Println, + logf: log.Printf, +} type repo struct { mu sync.Mutex @@ -29,6 +34,26 @@ type repo struct { logf func(format string, v ...any) } +// ListAll implements internal.UserRepo +func (*repo) ListAll() ([]internal.User, error) { + panic("unimplemented") +} + +// Lookup implements internal.UserRepo +func (*repo) Lookup(uid *suid.UUID) (internal.User, error) { + panic("unimplemented") +} + +// LookupEmail implements internal.UserRepo +func (*repo) LookupEmail(email string) (internal.User, error) { + panic("unimplemented") +} + +// Remove implements internal.UserRepo +func (*repo) Remove(uid *suid.UUID) error { + panic("unimplemented") +} + func (r *repo) Insert(ctx context.Context, iu *internal.User) error { r.mu.Lock() defer r.mu.Unlock() From 423ae236d92460323400025df40fefa54b2b4153 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 7 Oct 2022 01:39:31 +0330 Subject: [PATCH 161/246] fix refresh token flow --- service/auth/service.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/service/auth/service.go b/service/auth/service.go index 8a3cd8aa..37dc6992 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -88,10 +88,17 @@ func (s *Service) handleRefresh(key jwk.Key) http.HandlerFunc { err := s.tc.ValidateRefreshToken(r.Context(), k.Value) if err != nil { - s.respond(w, r, err, http.StatusTeapot) + s.respond(w, r, err, http.StatusInternalServerError) return } + // token validated, now it should be set inside blacklist + // this prevents token reuse + err = s.tc.BlackListRefreshToken(r.Context(), k.Value) + if err != nil { + s.respond(w, r, err, http.StatusInternalServerError) + } + id, _ := suid.ParseString(j.Subject()) _, ats, rts, err := s.signedTokens(key, e.String(), id) From 017bd7b2104ee75931cbddc66fd23938db0e6317 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 7 Oct 2022 01:44:53 +0330 Subject: [PATCH 162/246] uuid to suid for Client ID --- service/auth/service.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/service/auth/service.go b/service/auth/service.go index 37dc6992..3edee9d3 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -99,9 +99,8 @@ func (s *Service) handleRefresh(key jwk.Key) http.HandlerFunc { s.respond(w, r, err, http.StatusInternalServerError) } - id, _ := suid.ParseString(j.Subject()) - - _, ats, rts, err := s.signedTokens(key, e.String(), id) + cid := j.Subject() + _, ats, rts, err := s.signedTokens(key, e.String(), suid.SUID(cid)) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) return @@ -164,7 +163,7 @@ func (s *Service) handleSignIn(privateKey jwk.Key) http.HandlerFunc { return } - its, ats, rts, err := s.signedTokens(privateKey, u.Email.String(), suid.NewUUID()) + its, ats, rts, err := s.signedTokens(privateKey, u.Email.String(), suid.NewSUID()) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) return @@ -270,11 +269,11 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, func (s *Service) signedTokens( key jwk.Key, email string, - uuid suid.UUID, + cid suid.SUID, ) (its, ats, rts []byte, err error) { opt := auth.TokenOption{ Issuer: "github.com/rog-golang-buddies/rmx", - Subject: uuid.String(), // new client ID for tracking user connections + Subject: cid.String(), // new client ID for tracking user connections Expiration: time.Hour * 10, Claims: []fp.Tuple{{"email", email}}, Algo: jwa.ES256, From a6cd80ad382c4894454a92112907d10be14e9c64 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 7 Oct 2022 10:13:08 +0330 Subject: [PATCH 163/246] better API for CLI --- cmd/cli.go | 80 ------------------- cmd/env_test.go | 16 ---- cmd/main.go | 139 +++++----------------------------- config/config.go | 5 ++ internal/commands/commands.go | 59 +++++++++++++++ internal/commands/run.go | 124 ++++++++++++++++++++++++++++++ 6 files changed, 209 insertions(+), 214 deletions(-) delete mode 100644 cmd/cli.go delete mode 100644 cmd/env_test.go create mode 100644 config/config.go create mode 100644 internal/commands/commands.go create mode 100644 internal/commands/run.go diff --git a/cmd/cli.go b/cmd/cli.go deleted file mode 100644 index 1a8ebe53..00000000 --- a/cmd/cli.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "time" - - "github.com/urfave/cli/v2" - "github.com/urfave/cli/v2/altsrc" -) - -type Config struct { - Port int `json:"port"` -} - -var ( - ErrInvalidPort = errors.New("invalid port number") -) - -func initCLI() *cli.App { - flags := []cli.Flag{ - altsrc.NewIntFlag(&cli.IntFlag{ - Name: "port", - Value: 0, - Usage: "Defines the port which server should listen on", - Required: false, - Aliases: []string{"p"}, - EnvVars: []string{"PORT"}, - }), - &cli.StringFlag{ - Name: "load", - Aliases: []string{"l"}, - }, - } - - c := &cli.App{ - Name: "rmx", - Usage: "RapidMidiEx Server CLI", - Version: "v0.0.1", - Compiled: time.Now().UTC(), - Action: func(*cli.Context) error { - return nil - }, - Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")), - Commands: []*cli.Command{ - { - Name: "start", - Category: "run", - Aliases: []string{"s"}, - Description: "Starts the server in production mode.", - Action: func(cCtx *cli.Context) error { - port := cCtx.Int("port") - fmt.Println(port) - if port < 0 { - return ErrInvalidPort - } - - cfg := &Config{ - Port: port, - } - - return run(cfg) - }, - Flags: flags, - }, - { - Name: "dev", - Category: "run", - Aliases: []string{"d"}, - Description: "Starts the server in development mode", - Action: func(cCtx *cli.Context) error { - return nil - }, - Flags: flags, - }, - }, - } - - return c -} diff --git a/cmd/env_test.go b/cmd/env_test.go deleted file mode 100644 index 204ea968..00000000 --- a/cmd/env_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -// import ( -// "testing" -// ) - -// func TestEnv(t *testing.T) { -// // backend server address/port flag -// // frontend address/port flag -// // timeout - read, write, idle -// // - -// if err := loadConfig(); err != nil { -// t.Fatal(err) -// } -// } diff --git a/cmd/main.go b/cmd/main.go index 15d929d2..9cd2c2e4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,136 +1,39 @@ package main import ( - "context" "log" - "net" - "net/http" - "os/signal" - "strconv" - "syscall" + "os" "time" - "github.com/rs/cors" - "golang.org/x/sync/errgroup" + "github.com/rog-golang-buddies/rmx/internal/commands" + "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" ) -// var ( -// port = flag.Int("SERVER_PORT", 8888, "The port that the server will be running on") -// ) - func main() { - // if err := initCLI().Run(os.Args); err != nil { - if err := defaultRun(); err != nil { + if err := initCLI().Run(os.Args); err != nil { log.Fatalln(err) } } -func defaultRun() error { - sCtx, cancel := signal.NotifyContext( - context.Background(), - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - defer cancel() - - // ? should this defined within the instantiation of a new service - c := cors.Options{ - AllowedOrigins: []string{ - "http://localhost:8000", - }, // ? band-aid, needs to change to a flag - AllowCredentials: true, - AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete}, - AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, - } - - srv := http.Server{ - Addr: ":8080", - Handler: cors.New(c).Handler(http.DefaultServeMux), - // max time to read request from the client - ReadTimeout: 10 * time.Second, - // max time to write response to the client - WriteTimeout: 10 * time.Second, - // max time for connections using TCP Keep-Alive - IdleTimeout: 120 * time.Second, - BaseContext: func(_ net.Listener) context.Context { return sCtx }, - ErrorLog: log.Default(), +func initCLI() *cli.App { + c := &cli.App{ + EnableBashCompletion: true, + Name: "rmx", + Usage: "RapidMidiEx Server CLI", + Version: "v0.0.1", + Compiled: time.Now().UTC(), + Action: func(*cli.Context) error { + return nil + }, + Before: altsrc.InitInputSourceWithContext( + commands.Flags, + altsrc.NewYamlSourceFromFlagFunc("load"), + ), + Commands: commands.Commands, } - // srv.TLSConfig. - - g, gCtx := errgroup.WithContext(sCtx) - - g.Go(func() error { - // Run the server - srv.ErrorLog.Printf("App server starting on %s", srv.Addr) - return srv.ListenAndServe() - }) - - g.Go(func() error { - <-gCtx.Done() - return srv.Shutdown(context.Background()) - }) - - // if err := g.Wait(); err != nil { - // log.Printf("exit reason: %s \n", err) - // } - - return g.Wait() -} - -func run(cfg *Config) error { - sCtx, cancel := signal.NotifyContext( - context.Background(), - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - defer cancel() - - // ? should this defined within the instantiation of a new service - c := cors.Options{ - AllowedOrigins: []string{"*"}, // ? band-aid, needs to change to a flag - AllowCredentials: true, - AllowedMethods: []string{http.MethodGet, http.MethodPost}, - AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, - } - - srv := http.Server{ - Addr: ":" + strconv.Itoa(cfg.Port), - Handler: cors.New(c).Handler(http.NotFoundHandler()), - // max time to read request from the client - ReadTimeout: 10 * time.Second, - // max time to write response to the client - WriteTimeout: 10 * time.Second, - // max time for connections using TCP Keep-Alive - IdleTimeout: 120 * time.Second, - BaseContext: func(_ net.Listener) context.Context { return sCtx }, - ErrorLog: log.Default(), - } - - // srv.TLSConfig. - - g, gCtx := errgroup.WithContext(sCtx) - - g.Go(func() error { - // Run the server - srv.ErrorLog.Printf("App server starting on %s", srv.Addr) - return srv.ListenAndServe() - }) - - g.Go(func() error { - <-gCtx.Done() - return srv.Shutdown(context.Background()) - }) - - // if err := g.Wait(); err != nil { - // log.Printf("exit reason: %s \n", err) - // } - - return g.Wait() + return c } // func getEnv(key, fallback string) string { diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..c5fe8eaa --- /dev/null +++ b/config/config.go @@ -0,0 +1,5 @@ +package config + +type Config struct { + Port int `json:"port"` +} diff --git a/internal/commands/commands.go b/internal/commands/commands.go new file mode 100644 index 00000000..964499e0 --- /dev/null +++ b/internal/commands/commands.go @@ -0,0 +1,59 @@ +package commands + +import ( + "errors" + + "github.com/rog-golang-buddies/rmx/config" + "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" +) + +var ( + ErrInvalidPort = errors.New("invalid port number") +) + +var Flags = []cli.Flag{ + altsrc.NewIntFlag(&cli.IntFlag{ + Name: "port", + Value: 0, + Usage: "Defines the port which server should listen on", + Required: false, + Aliases: []string{"p"}, + EnvVars: []string{"PORT"}, + }), + &cli.StringFlag{ + Name: "load", + Aliases: []string{"l"}, + }, +} + +var Commands = []*cli.Command{ + { + Name: "start", + Category: "run", + Aliases: []string{"s"}, + Description: "Starts the server in production mode.", + Action: func(cCtx *cli.Context) error { + port := cCtx.Int("port") + if port < 0 { + return ErrInvalidPort + } + + cfg := &config.Config{ + Port: port, + } + return runProd(cfg) + }, + Flags: Flags, + }, + { + Name: "dev", + Category: "run", + Aliases: []string{"d"}, + Description: "Starts the server in development mode", + Action: func(cCtx *cli.Context) error { + return runDev() + }, + Flags: Flags, + }, +} diff --git a/internal/commands/run.go b/internal/commands/run.go new file mode 100644 index 00000000..2d187cea --- /dev/null +++ b/internal/commands/run.go @@ -0,0 +1,124 @@ +package commands + +import ( + "context" + "log" + "net" + "net/http" + "os/signal" + "strconv" + "syscall" + "time" + + "github.com/rog-golang-buddies/rmx/config" + "github.com/rs/cors" + "golang.org/x/sync/errgroup" +) + +func runProd(cfg *config.Config) error { + sCtx, cancel := signal.NotifyContext( + context.Background(), + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + defer cancel() + + // ? should this defined within the instantiation of a new service + c := cors.Options{ + AllowedOrigins: []string{"*"}, // ? band-aid, needs to change to a flag + AllowCredentials: true, + AllowedMethods: []string{http.MethodGet, http.MethodPost}, + AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + } + + srv := http.Server{ + Addr: ":" + strconv.Itoa(cfg.Port), + Handler: cors.New(c).Handler(http.NotFoundHandler()), + // max time to read request from the client + ReadTimeout: 10 * time.Second, + // max time to write response to the client + WriteTimeout: 10 * time.Second, + // max time for connections using TCP Keep-Alive + IdleTimeout: 120 * time.Second, + BaseContext: func(_ net.Listener) context.Context { return sCtx }, + ErrorLog: log.Default(), + } + + // srv.TLSConfig. + + g, gCtx := errgroup.WithContext(sCtx) + + g.Go(func() error { + // Run the server + srv.ErrorLog.Printf("App server starting on %s", srv.Addr) + return srv.ListenAndServe() + }) + + g.Go(func() error { + <-gCtx.Done() + return srv.Shutdown(context.Background()) + }) + + // if err := g.Wait(); err != nil { + // log.Printf("exit reason: %s \n", err) + // } + + return g.Wait() +} + +func runDev() error { + sCtx, cancel := signal.NotifyContext( + context.Background(), + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + defer cancel() + + // ? should this defined within the instantiation of a new service + c := cors.Options{ + AllowedOrigins: []string{ + "http://localhost:8000", + }, // ? band-aid, needs to change to a flag + AllowCredentials: true, + AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete}, + AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + } + + srv := http.Server{ + Addr: ":8080", + Handler: cors.New(c).Handler(http.DefaultServeMux), + // max time to read request from the client + ReadTimeout: 10 * time.Second, + // max time to write response to the client + WriteTimeout: 10 * time.Second, + // max time for connections using TCP Keep-Alive + IdleTimeout: 120 * time.Second, + BaseContext: func(_ net.Listener) context.Context { return sCtx }, + ErrorLog: log.Default(), + } + + // srv.TLSConfig. + + g, gCtx := errgroup.WithContext(sCtx) + + g.Go(func() error { + // Run the server + srv.ErrorLog.Printf("App server starting on %s", srv.Addr) + return srv.ListenAndServe() + }) + + g.Go(func() error { + <-gCtx.Done() + return srv.Shutdown(context.Background()) + }) + + // if err := g.Wait(); err != nil { + // log.Printf("exit reason: %s \n", err) + // } + + return g.Wait() +} From b3330d492503f6bb25a5a9dfadd6cc36f9ea2c03 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 7 Oct 2022 08:30:30 +0100 Subject: [PATCH 164/246] store cleanup --- store/auth/auth.go | 16 +++++++++++----- store/store.go | 17 ++++++++++++----- store/user/user.go | 14 ++++++++++++++ 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/store/auth/auth.go b/store/auth/auth.go index b6b662ee..72824c09 100644 --- a/store/auth/auth.go +++ b/store/auth/auth.go @@ -9,8 +9,14 @@ import ( "github.com/lestrrat-go/jwx/v2/jwt" ) +var DefaultClient = &client{make(map[string]bool), make(map[string]bool)} + +func (c *client) Validate(ctx context.Context, token string) error { + return ErrNotImplemented +} + type client struct { - rt, ci *redis.Client + mrt, mci map[string]bool } type Client struct { @@ -37,10 +43,10 @@ const ( defaultPassword = "" ) -var DefaultClient = &Client{ - rtdb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 0}), - cidb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 1}), -} +// var DefaultClient = &Client{ +// rtdb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 0}), +// cidb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 1}), +// } func (c *Client) Validate(ctx context.Context, token string) error { tc, err := ParseRefreshTokenClaims(token) diff --git a/store/store.go b/store/store.go index 97078565..bfcc0b55 100644 --- a/store/store.go +++ b/store/store.go @@ -1,10 +1,17 @@ package store -import "context" +import ( + "context" -type Store struct{} + "github.com/rog-golang-buddies/rmx/internal" +) -func New(ctx context.Context) *Store { - db := &Store{} - return db +type Store struct { + tc internal.TokenClient + ur internal.UserRepo +} + +func New(ctx context.Context, connString string) *Store { + s := &Store{} + return s } diff --git a/store/user/user.go b/store/user/user.go index 677b8ff6..8036ccde 100644 --- a/store/user/user.go +++ b/store/user/user.go @@ -2,14 +2,28 @@ package user import ( "context" + "database/sql" "log" "sync" "time" "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/suid" + + "github.com/rog-golang-buddies/rmx/store/sql/user" ) +type Repo struct { + c *sql.DB +} + +func New(ctx context.Context) internal.UserRepo { + var conn *sql.DB + + user.New(conn) + return nil +} + type User struct { ID suid.UUID Username string From 7d5507ba6cf44c4df2fd9ee163ccd62a5c571f03 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 7 Oct 2022 08:30:48 +0100 Subject: [PATCH 165/246] auth service cleanup --- service/auth/service.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/service/auth/service.go b/service/auth/service.go index 8753b05d..66a31ad6 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -37,6 +37,14 @@ Get current account identity [?] GET /account/me +Delete devices linked to account + + [ ] DELETE /account/{uuid}/device + +this returns a list of current connections: + + [ ] GET /account/{uuid}/devices + Create a cookie [?] POST /auth/sign-in From 38727b9eebe7f2821c84932b130eb24b32d396b9 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 7 Oct 2022 11:52:01 +0330 Subject: [PATCH 166/246] CLI prompts --- config/config.go | 10 +- go.mod | 2 + go.sum | 9 ++ internal/commands/commands.go | 15 +-- internal/commands/run.go | 235 ++++++++++++++++++++++++++++------ 5 files changed, 216 insertions(+), 55 deletions(-) diff --git a/config/config.go b/config/config.go index c5fe8eaa..1d6fd03f 100644 --- a/config/config.go +++ b/config/config.go @@ -1,5 +1,13 @@ package config type Config struct { - Port int `json:"port"` + ServerPort string `json:"serverPort"` + DBHost string `json:"dbHost"` + DBPort string `json:"dbPort"` + DBName string `json:"dbName"` + DBUser string `json:"dbUser"` + DBPassword string `json:"dbPassword"` + RedisHost string `json:"redisHost"` + RedisPort string `json:"redisPort"` + RedisPassword string `json:"redisPassword"` } diff --git a/go.mod b/go.mod index d336945e..d2072352 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/hyphengolang/prelude v0.0.7 github.com/lithammer/shortuuid/v4 v4.0.0 + github.com/manifoldco/promptui v0.9.0 github.com/pkg/errors v0.9.1 github.com/rs/cors v1.8.2 github.com/urfave/cli/v2 v2.16.3 @@ -23,6 +24,7 @@ require ( require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/goccy/go-json v0.9.11 // indirect diff --git a/go.sum b/go.sum index e2cd8a55..740a3f75 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,12 @@ github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJ github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY= github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= @@ -57,6 +63,8 @@ github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -113,6 +121,7 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 964499e0..475cc856 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -3,7 +3,6 @@ package commands import ( "errors" - "github.com/rog-golang-buddies/rmx/config" "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" ) @@ -33,18 +32,8 @@ var Commands = []*cli.Command{ Category: "run", Aliases: []string{"s"}, Description: "Starts the server in production mode.", - Action: func(cCtx *cli.Context) error { - port := cCtx.Int("port") - if port < 0 { - return ErrInvalidPort - } - - cfg := &config.Config{ - Port: port, - } - return runProd(cfg) - }, - Flags: Flags, + Action: runProd, + Flags: Flags, }, { Name: "dev", diff --git a/internal/commands/run.go b/internal/commands/run.go index 2d187cea..afce6d3a 100644 --- a/internal/commands/run.go +++ b/internal/commands/run.go @@ -2,6 +2,7 @@ package commands import ( "context" + "errors" "log" "net" "net/http" @@ -10,62 +11,214 @@ import ( "syscall" "time" + "github.com/manifoldco/promptui" "github.com/rog-golang-buddies/rmx/config" "github.com/rs/cors" + "github.com/urfave/cli/v2" "golang.org/x/sync/errgroup" ) -func runProd(cfg *config.Config) error { - sCtx, cancel := signal.NotifyContext( - context.Background(), - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - defer cancel() +func runProd(cCtx *cli.Context) error { + templates := &promptui.PromptTemplates{ + Prompt: "{{ . }} ", + Valid: "{{ . | green }} ", + Invalid: "{{ . | red }} ", + Success: "{{ . | bold }} ", + } - // ? should this defined within the instantiation of a new service - c := cors.Options{ - AllowedOrigins: []string{"*"}, // ? band-aid, needs to change to a flag - AllowCredentials: true, - AllowedMethods: []string{http.MethodGet, http.MethodPost}, - AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + // Server Port + validateNumber := func(v string) error { + if _, err := strconv.ParseUint(v, 0, 0); err != nil { + return errors.New("invalid number") + } + + return nil } - srv := http.Server{ - Addr: ":" + strconv.Itoa(cfg.Port), - Handler: cors.New(c).Handler(http.NotFoundHandler()), - // max time to read request from the client - ReadTimeout: 10 * time.Second, - // max time to write response to the client - WriteTimeout: 10 * time.Second, - // max time for connections using TCP Keep-Alive - IdleTimeout: 120 * time.Second, - BaseContext: func(_ net.Listener) context.Context { return sCtx }, - ErrorLog: log.Default(), + validateString := func(v string) error { + if !(len(v) > 0) { + return errors.New("invalid string") + } + + return nil } - // srv.TLSConfig. + // Server Port + serverPortPrompt := promptui.Prompt{ + Label: "Server Port", + Validate: validateNumber, + Templates: templates, + } - g, gCtx := errgroup.WithContext(sCtx) + serverPort, err := serverPortPrompt.Run() + if err != nil { + return err + } - g.Go(func() error { - // Run the server - srv.ErrorLog.Printf("App server starting on %s", srv.Addr) - return srv.ListenAndServe() - }) + // DB Host + dbHostPrompt := promptui.Prompt{ + Label: "MySQL Database host", + Validate: validateString, + Templates: templates, + } - g.Go(func() error { - <-gCtx.Done() - return srv.Shutdown(context.Background()) - }) + dbHost, err := dbHostPrompt.Run() + if err != nil { + return err + } - // if err := g.Wait(); err != nil { - // log.Printf("exit reason: %s \n", err) - // } + // DB Port + dbPortPrompt := promptui.Prompt{ + Label: "MySQL Database port", + Validate: validateNumber, + Templates: templates, + } - return g.Wait() + dbPort, err := dbPortPrompt.Run() + if err != nil { + return err + } + + // DB Name + dbNamePrompt := promptui.Prompt{ + Label: "MySQL Database name", + Validate: validateString, + Templates: templates, + } + + dbName, err := dbNamePrompt.Run() + if err != nil { + return err + } + + // DB User + dbUserPrompt := promptui.Prompt{ + Label: "MySQL Database user", + Validate: validateString, + Templates: templates, + } + + dbUser, err := dbUserPrompt.Run() + if err != nil { + return err + } + + // DB Password + dbPasswordPrompt := promptui.Prompt{ + Label: "MySQL Database password", + Validate: validateString, + Templates: templates, + Mask: '*', + } + + dbPassword, err := dbPasswordPrompt.Run() + if err != nil { + return err + } + + // Redis Host + redisHostPrompt := promptui.Prompt{ + Label: "Redis host", + Validate: validateString, + Templates: templates, + } + + redisHost, err := redisHostPrompt.Run() + if err != nil { + return err + } + + // Redis Port + redisPortPrompt := promptui.Prompt{ + Label: "Redis port", + Validate: validateNumber, + Templates: templates, + } + + redisPort, err := redisPortPrompt.Run() + if err != nil { + return err + } + + // Redis Password + redisPasswordPrompt := promptui.Prompt{ + Label: "Redis password", + Validate: validateString, + Templates: templates, + Mask: '*', + } + + redisPassword, err := redisPasswordPrompt.Run() + if err != nil { + return err + } + + c := &config.Config{ + ServerPort: ":" + serverPort, + DBHost: dbHost, + DBPort: dbPort, + DBName: dbName, + DBUser: dbUser, + DBPassword: dbPassword, + RedisHost: redisHost, + RedisPort: redisPort, + RedisPassword: redisPassword, + } + + run := func(cfg *config.Config) error { + sCtx, cancel := signal.NotifyContext( + context.Background(), + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + defer cancel() + + // ? should this defined within the instantiation of a new service + c := cors.Options{ + AllowedOrigins: []string{"*"}, // ? band-aid, needs to change to a flag + AllowCredentials: true, + AllowedMethods: []string{http.MethodGet, http.MethodPost}, + AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + } + + srv := http.Server{ + Addr: cfg.ServerPort, + Handler: cors.New(c).Handler(http.NotFoundHandler()), + // max time to read request from the client + ReadTimeout: 10 * time.Second, + // max time to write response to the client + WriteTimeout: 10 * time.Second, + // max time for connections using TCP Keep-Alive + IdleTimeout: 120 * time.Second, + BaseContext: func(_ net.Listener) context.Context { return sCtx }, + ErrorLog: log.Default(), + } + + // srv.TLSConfig. + + g, gCtx := errgroup.WithContext(sCtx) + + g.Go(func() error { + // Run the server + srv.ErrorLog.Printf("App server starting on %s", srv.Addr) + return srv.ListenAndServe() + }) + + g.Go(func() error { + <-gCtx.Done() + return srv.Shutdown(context.Background()) + }) + + // if err := g.Wait(); err != nil { + // log.Printf("exit reason: %s \n", err) + // } + + return g.Wait() + } + + return run(c) } func runDev() error { From 483ea4536b4a2e94d1721b89a09ba324126f60aa Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 7 Oct 2022 14:10:58 +0330 Subject: [PATCH 167/246] write/load configuration to/from file --- .gitignore | 1 + config/config.go | 58 ++++++++++++++ internal/commands/run.go | 161 +++++++++++++++++++++++++++------------ 3 files changed, 172 insertions(+), 48 deletions(-) diff --git a/.gitignore b/.gitignore index 1fb33db8..7aa2f335 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ bin/ *.env cmd/**/config certs +rmx.config.json # NPM node_modules diff --git a/config/config.go b/config/config.go index 1d6fd03f..36ca39b7 100644 --- a/config/config.go +++ b/config/config.go @@ -1,5 +1,12 @@ package config +import ( + "encoding/json" + "errors" + "log" + "os" +) + type Config struct { ServerPort string `json:"serverPort"` DBHost string `json:"dbHost"` @@ -11,3 +18,54 @@ type Config struct { RedisPort string `json:"redisPort"` RedisPassword string `json:"redisPassword"` } + +const configFileName = "rmx.config.json" + +// writes the values of the config to a file +// NOTE: this will overwrite the previous generated file +func (c *Config) WriteToFile() error { + bs, err := json.MarshalIndent(c, "", " ") + if err != nil { + return err + } + + f, err := os.OpenFile(configFileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0660) + if err != nil { + return err + } + + defer func() { + if err := f.Close(); err != nil { + log.Fatalln(err) + } + }() + + if _, err := f.Write(bs); err != nil { + return err + } + + return nil +} + +// checks for a config file and if one is available the value is returned +func ScanConfigFile() (*Config, error) { + // check for a config file + if _, err := os.Stat(configFileName); err == nil { + } else if errors.Is(err, os.ErrNotExist) { + return nil, nil + } else { + return nil, err + } + + c := &Config{} + bs, err := os.ReadFile(configFileName) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(bs, c); err != nil { + return nil, err + } + + return c, nil +} diff --git a/internal/commands/run.go b/internal/commands/run.go index afce6d3a..9325ee91 100644 --- a/internal/commands/run.go +++ b/internal/commands/run.go @@ -8,6 +8,7 @@ import ( "net/http" "os/signal" "strconv" + "strings" "syscall" "time" @@ -19,6 +20,59 @@ import ( ) func runProd(cCtx *cli.Context) error { + run := func(cfg *config.Config) error { + sCtx, cancel := signal.NotifyContext( + context.Background(), + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + defer cancel() + + // ? should this defined within the instantiation of a new service + c := cors.Options{ + AllowedOrigins: []string{"*"}, // ? band-aid, needs to change to a flag + AllowCredentials: true, + AllowedMethods: []string{http.MethodGet, http.MethodPost}, + AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + } + + srv := http.Server{ + Addr: ":" + cfg.ServerPort, + Handler: cors.New(c).Handler(http.NotFoundHandler()), + // max time to read request from the client + ReadTimeout: 10 * time.Second, + // max time to write response to the client + WriteTimeout: 10 * time.Second, + // max time for connections using TCP Keep-Alive + IdleTimeout: 120 * time.Second, + BaseContext: func(_ net.Listener) context.Context { return sCtx }, + ErrorLog: log.Default(), + } + + // srv.TLSConfig. + + g, gCtx := errgroup.WithContext(sCtx) + + g.Go(func() error { + // Run the server + srv.ErrorLog.Printf("App server starting on %s", srv.Addr) + return srv.ListenAndServe() + }) + + g.Go(func() error { + <-gCtx.Done() + return srv.Shutdown(context.Background()) + }) + + // if err := g.Wait(); err != nil { + // log.Printf("exit reason: %s \n", err) + // } + + return g.Wait() + } + templates := &promptui.PromptTemplates{ Prompt: "{{ . }} ", Valid: "{{ . | green }} ", @@ -43,6 +97,40 @@ func runProd(cCtx *cli.Context) error { return nil } + // check if a config file exists and use that + c, err := config.ScanConfigFile() + if err != nil { + return errors.New("failed to scan config file") + } + if c != nil { + configPrompt := promptui.Prompt{ + Label: "A config file was found. do you want to use it?", + IsConfirm: true, + Default: "y", + } + + validateConfirm := func(s string) error { + if len(s) == 1 && strings.Contains("YyNn", s) || + configPrompt.Default != "" && len(s) == 0 { + return nil + } + return errors.New("invalid input") + } + + configPrompt.Validate = validateConfirm + + result, err := configPrompt.Run() + if err != nil { + if strings.ToLower(result) != "n" { + return err + } + } + + if strings.ToLower(result) == "y" { + return run(c) + } + } + // Server Port serverPortPrompt := promptui.Prompt{ Label: "Server Port", @@ -153,8 +241,8 @@ func runProd(cCtx *cli.Context) error { return err } - c := &config.Config{ - ServerPort: ":" + serverPort, + c = &config.Config{ + ServerPort: serverPort, DBHost: dbHost, DBPort: dbPort, DBName: dbName, @@ -165,57 +253,34 @@ func runProd(cCtx *cli.Context) error { RedisPassword: redisPassword, } - run := func(cfg *config.Config) error { - sCtx, cancel := signal.NotifyContext( - context.Background(), - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - defer cancel() + // prompt to save the config to a file + configPrompt := promptui.Prompt{ + Label: "Do you want to write the config to a file? (NOTE: this will rewrite the config file)", + IsConfirm: true, + Default: "n", + } - // ? should this defined within the instantiation of a new service - c := cors.Options{ - AllowedOrigins: []string{"*"}, // ? band-aid, needs to change to a flag - AllowCredentials: true, - AllowedMethods: []string{http.MethodGet, http.MethodPost}, - AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + validateConfirm := func(s string) error { + if len(s) == 1 && strings.Contains("YyNn", s) || + configPrompt.Default != "" && len(s) == 0 { + return nil } + return errors.New("invalid input") + } - srv := http.Server{ - Addr: cfg.ServerPort, - Handler: cors.New(c).Handler(http.NotFoundHandler()), - // max time to read request from the client - ReadTimeout: 10 * time.Second, - // max time to write response to the client - WriteTimeout: 10 * time.Second, - // max time for connections using TCP Keep-Alive - IdleTimeout: 120 * time.Second, - BaseContext: func(_ net.Listener) context.Context { return sCtx }, - ErrorLog: log.Default(), - } - - // srv.TLSConfig. - - g, gCtx := errgroup.WithContext(sCtx) - - g.Go(func() error { - // Run the server - srv.ErrorLog.Printf("App server starting on %s", srv.Addr) - return srv.ListenAndServe() - }) - - g.Go(func() error { - <-gCtx.Done() - return srv.Shutdown(context.Background()) - }) + configPrompt.Validate = validateConfirm - // if err := g.Wait(); err != nil { - // log.Printf("exit reason: %s \n", err) - // } + result, err := configPrompt.Run() + if err != nil { + if strings.ToLower(result) != "n" { + return err + } + } - return g.Wait() + if strings.ToLower(result) == "y" { + if err := c.WriteToFile(); err != nil { + return err + } } return run(c) From 9f8f9ff161e9f59dee4dc0e89d204babbf1ee9a5 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 7 Oct 2022 14:12:00 +0330 Subject: [PATCH 168/246] fix sqlc.yaml --- sqlc.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlc.yaml b/sqlc.yaml index 9075e5b3..c240450d 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -1,12 +1,12 @@ version: '2' sql: - - schema: 'store/db/schema/user_schema.sql' - queries: 'store/db/schema/user_query.sql' + - schema: 'store/sql/schema/user_schema.sql' + queries: 'store/sql/schema/user_query.sql' engine: 'mysql' gen: go: package: 'user' - out: 'store/db/user' + out: 'store/sql/user' emit_db_tags: true emit_prepared_queries: true emit_empty_slices: true From 1a014549a10af9ea692d0e25b16810fa9ad8b3a1 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 7 Oct 2022 14:22:24 +0330 Subject: [PATCH 169/246] fix small bug --- config/config.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index 36ca39b7..e90e3c0d 100644 --- a/config/config.go +++ b/config/config.go @@ -50,10 +50,11 @@ func (c *Config) WriteToFile() error { // checks for a config file and if one is available the value is returned func ScanConfigFile() (*Config, error) { // check for a config file - if _, err := os.Stat(configFileName); err == nil { - } else if errors.Is(err, os.ErrNotExist) { - return nil, nil - } else { + if _, err := os.Stat(configFileName); err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } + return nil, err } From f00db328c7cb003ba11631e4303f9a9e54a41a5c Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 7 Oct 2022 23:45:06 +0330 Subject: [PATCH 170/246] clean run file --- internal/commands/run.go | 83 +++++++++++----------------------------- 1 file changed, 22 insertions(+), 61 deletions(-) diff --git a/internal/commands/run.go b/internal/commands/run.go index 9325ee91..e32021e6 100644 --- a/internal/commands/run.go +++ b/internal/commands/run.go @@ -20,59 +20,6 @@ import ( ) func runProd(cCtx *cli.Context) error { - run := func(cfg *config.Config) error { - sCtx, cancel := signal.NotifyContext( - context.Background(), - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - defer cancel() - - // ? should this defined within the instantiation of a new service - c := cors.Options{ - AllowedOrigins: []string{"*"}, // ? band-aid, needs to change to a flag - AllowCredentials: true, - AllowedMethods: []string{http.MethodGet, http.MethodPost}, - AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, - } - - srv := http.Server{ - Addr: ":" + cfg.ServerPort, - Handler: cors.New(c).Handler(http.NotFoundHandler()), - // max time to read request from the client - ReadTimeout: 10 * time.Second, - // max time to write response to the client - WriteTimeout: 10 * time.Second, - // max time for connections using TCP Keep-Alive - IdleTimeout: 120 * time.Second, - BaseContext: func(_ net.Listener) context.Context { return sCtx }, - ErrorLog: log.Default(), - } - - // srv.TLSConfig. - - g, gCtx := errgroup.WithContext(sCtx) - - g.Go(func() error { - // Run the server - srv.ErrorLog.Printf("App server starting on %s", srv.Addr) - return srv.ListenAndServe() - }) - - g.Go(func() error { - <-gCtx.Done() - return srv.Shutdown(context.Background()) - }) - - // if err := g.Wait(); err != nil { - // log.Printf("exit reason: %s \n", err) - // } - - return g.Wait() - } - templates := &promptui.PromptTemplates{ Prompt: "{{ . }} ", Valid: "{{ . | green }} ", @@ -114,7 +61,7 @@ func runProd(cCtx *cli.Context) error { configPrompt.Default != "" && len(s) == 0 { return nil } - return errors.New("invalid input") + return errors.New(`invalid input (you can only use "y" or "n")`) } configPrompt.Validate = validateConfirm @@ -265,7 +212,7 @@ func runProd(cCtx *cli.Context) error { configPrompt.Default != "" && len(s) == 0 { return nil } - return errors.New("invalid input") + return errors.New(`invalid input (you can only use "y" or "n")`) } configPrompt.Validate = validateConfirm @@ -287,6 +234,22 @@ func runProd(cCtx *cli.Context) error { } func runDev() error { + c := &config.Config{ + ServerPort: "8080", + DBHost: "localhost", + DBPort: "3306", + DBName: "rmx", + DBUser: "rmx", + DBPassword: "password", + RedisHost: "localhost", + RedisPort: "6379", + RedisPassword: "password", + } + + return run(c) +} + +func run(cfg *config.Config) error { sCtx, cancel := signal.NotifyContext( context.Background(), syscall.SIGHUP, @@ -298,17 +261,15 @@ func runDev() error { // ? should this defined within the instantiation of a new service c := cors.Options{ - AllowedOrigins: []string{ - "http://localhost:8000", - }, // ? band-aid, needs to change to a flag + AllowedOrigins: []string{"*"}, // ? band-aid, needs to change to a flag AllowCredentials: true, - AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodDelete}, + AllowedMethods: []string{http.MethodGet, http.MethodPost}, AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, } srv := http.Server{ - Addr: ":8080", - Handler: cors.New(c).Handler(http.DefaultServeMux), + Addr: ":" + cfg.ServerPort, + Handler: cors.New(c).Handler(http.NotFoundHandler()), // max time to read request from the client ReadTimeout: 10 * time.Second, // max time to write response to the client From 5f7408b3890078f028b30cc66e7b5559c0797621 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 7 Oct 2022 23:48:49 +0330 Subject: [PATCH 171/246] =?UTF-8?q?=F0=9F=8D=95=20/api/v2=20->=20/api/v1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- service/auth/service.go | 4 ++-- service/auth/service_test.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/service/auth/service.go b/service/auth/service.go index 3edee9d3..4c419889 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -52,7 +52,7 @@ Refresh token func (s *Service) routes() { key := auth.NewPairES256() - s.m.Route("/api/v2/auth", func(r chi.Router) { + s.m.Route("/api/v1/auth", func(r chi.Router) { r.Post("/sign-in", s.handleSignIn(key.Private())) r.Delete("/sign-out", s.handleSignOut()) r.Post("/sign-up", s.handleSignUp()) @@ -63,7 +63,7 @@ func (s *Service) routes() { auth.Get("/refresh", s.handleRefresh(key.Private())) }) - s.m.Route("/api/v2/account", func(r chi.Router) { + s.m.Route("/api/v1/account", func(r chi.Router) { auth := r.With(auth.ParseAuth(jwa.ES256, key.Public())) auth.Get("/me", s.handleIdentity()) }) diff --git a/service/auth/service_test.go b/service/auth/service_test.go index ee9f47cb..d0786ae7 100644 --- a/service/auth/service_test.go +++ b/service/auth/service_test.go @@ -39,7 +39,7 @@ func TestService(t *testing.T) { }` res, _ := srv.Client(). - Post(srv.URL+"/api/v2/auth/sign-up", applicationJson, strings.NewReader(payload)) + Post(srv.URL+"/api/v1/auth/sign-up", applicationJson, strings.NewReader(payload)) is.Equal(res.StatusCode, http.StatusCreated) }) @@ -51,7 +51,7 @@ func TestService(t *testing.T) { }` res, _ := srv.Client(). - Post(srv.URL+"/api/v2/auth/sign-in", applicationJson, strings.NewReader(payload)) + Post(srv.URL+"/api/v1/auth/sign-in", applicationJson, strings.NewReader(payload)) is.Equal(res.StatusCode, http.StatusOK) type body struct { @@ -64,12 +64,12 @@ func TestService(t *testing.T) { res.Body.Close() is.NoErr(err) // parsing json - req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/v2/account/me", nil) + req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/v1/account/me", nil) req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, b.AccessToken)) res, _ = srv.Client().Do(req) is.Equal(res.StatusCode, http.StatusOK) // authorized endpoint - req, _ = http.NewRequest(http.MethodDelete, srv.URL+"/api/v2/auth/sign-out", nil) + req, _ = http.NewRequest(http.MethodDelete, srv.URL+"/api/v1/auth/sign-out", nil) req.Header.Set(`Authorization`, fmt.Sprintf(`Bearer %s`, b.AccessToken)) res, _ = srv.Client().Do(req) is.Equal(res.StatusCode, http.StatusOK) // delete cookie @@ -83,7 +83,7 @@ func TestService(t *testing.T) { }` res, _ := srv.Client(). - Post(srv.URL+"/api/v2/auth/sign-in", applicationJson, strings.NewReader(payload)) + Post(srv.URL+"/api/v1/auth/sign-in", applicationJson, strings.NewReader(payload)) is.Equal(res.StatusCode, http.StatusOK) // add refresh token // get the refresh token from the response's `Set-Cookie` header @@ -95,7 +95,7 @@ func TestService(t *testing.T) { } } - req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/v2/auth/refresh", nil) + req, _ := http.NewRequest(http.MethodGet, srv.URL+"/api/v1/auth/refresh", nil) req.AddCookie(c) res, _ = srv.Client().Do(req) From 2c0c3ae1f071935a9cec32cd63cfccd535ad752a Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 7 Oct 2022 21:34:39 +0100 Subject: [PATCH 172/246] store && service --- service/service.go | 6 +++++- store/auth/auth.go | 16 +++++++++++++++- store/store.go | 8 ++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/service/service.go b/service/service.go index 76fe8723..a6cca6a4 100644 --- a/service/service.go +++ b/service/service.go @@ -1,11 +1,13 @@ package service import ( + "context" "log" "net/http" "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" + "github.com/rog-golang-buddies/rmx/service/auth" "github.com/rog-golang-buddies/rmx/store" ) @@ -24,8 +26,10 @@ type Service struct { func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func New(st store.Store) *Service { +func New(ctx context.Context, st store.Store) http.Handler { s := &Service{chi.NewMux(), log.Print, log.Printf, log.Fatal, log.Fatalf} s.routes() + + auth.NewService(ctx, s.m, st.UserRepo(), st.TokenClient()) return s } diff --git a/store/auth/auth.go b/store/auth/auth.go index 1c431453..fd308791 100644 --- a/store/auth/auth.go +++ b/store/auth/auth.go @@ -11,10 +11,24 @@ import ( var DefaultClient = &client{make(map[string]bool), make(map[string]bool)} -func (c *client) Validate(ctx context.Context, token string) error { +func (c *client) ValidateRefreshToken(ctx context.Context, token string) error { return ErrNotImplemented } +func (c *client) ValidateClientID(ctx context.Context, token string) error { + return ErrNotImplemented +} + +// BlackListClientID implements internal.TokenClient +func (c *client) BlackListClientID(ctx context.Context, cid string, email string) error { + panic("unimplemented") +} + +// BlackListRefreshToken implements internal.TokenClient +func (c *client) BlackListRefreshToken(ctx context.Context, token string) error { + panic("unimplemented") +} + type client struct { mrt, mci map[string]bool } diff --git a/store/store.go b/store/store.go index bfcc0b55..074c2c2d 100644 --- a/store/store.go +++ b/store/store.go @@ -11,6 +11,14 @@ type Store struct { ur internal.UserRepo } +func (s *Store) UserRepo() internal.UserRepo { + return s.ur +} + +func (s *Store) TokenClient() internal.TokenClient { + return s.tc +} + func New(ctx context.Context, connString string) *Store { s := &Store{} return s From e2a5bdf2d4f0291977f0885482a2b4275cef5173 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 7 Oct 2022 21:46:15 +0100 Subject: [PATCH 173/246] no need for `Lookup` as `Select` already does this --- internal/internal.go | 2 -- store/user/user.go | 14 ++------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/internal/internal.go b/internal/internal.go index 241f21dc..71e3e5db 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -119,8 +119,6 @@ type UserRepo interface { } type RUserRepo interface { - Lookup(uid *suid.UUID) (User, error) - LookupEmail(email string) (User, error) ListAll() ([]User, error) Select(ctx context.Context, key any) (*User, error) } diff --git a/store/user/user.go b/store/user/user.go index 2ae310e8..0dd6ebbf 100644 --- a/store/user/user.go +++ b/store/user/user.go @@ -49,22 +49,12 @@ type repo struct { } // ListAll implements internal.UserRepo -func (*repo) ListAll() ([]internal.User, error) { - panic("unimplemented") -} - -// Lookup implements internal.UserRepo -func (*repo) Lookup(uid *suid.UUID) (internal.User, error) { - panic("unimplemented") -} - -// LookupEmail implements internal.UserRepo -func (*repo) LookupEmail(email string) (internal.User, error) { +func (r *repo) ListAll() ([]internal.User, error) { panic("unimplemented") } // Remove implements internal.UserRepo -func (*repo) Remove(uid *suid.UUID) error { +func (r *repo) Remove(uid *suid.UUID) error { panic("unimplemented") } From 3cb3eec2c5a3141854dc29e0396148d6cd3f3af6 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 7 Oct 2022 21:46:42 +0100 Subject: [PATCH 174/246] update service --- service/jam/service.go | 2 +- service/jam/service_test.go | 3 ++- service/service.go | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/service/jam/service.go b/service/jam/service.go index 42ac53f5..e97b58b4 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -234,7 +234,7 @@ type Service struct { func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func NewService(r chi.Router) *Service { +func NewService(ctx context.Context, r chi.Router) *Service { s := &Service{ r, ws.DefaultClient, diff --git a/service/jam/service_test.go b/service/jam/service_test.go index 3d81ce97..103568ec 100644 --- a/service/jam/service_test.go +++ b/service/jam/service_test.go @@ -1,6 +1,7 @@ package jam import ( + "context" "net/http/httptest" "testing" @@ -9,7 +10,7 @@ import ( ) func TestRoutes(t *testing.T) { - srv := NewService(chi.NewMux()) + srv := NewService(context.Background(), chi.NewMux()) // srv.r.Get("/ws/echo", chain(srv.handleEcho(),srv.upgradeHTTP(1024,1024),srv.connectionPool(websocket.DefaultPool()))) diff --git a/service/service.go b/service/service.go index a6cca6a4..f84545db 100644 --- a/service/service.go +++ b/service/service.go @@ -8,6 +8,7 @@ import ( "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" "github.com/rog-golang-buddies/rmx/service/auth" + "github.com/rog-golang-buddies/rmx/service/jam" "github.com/rog-golang-buddies/rmx/store" ) @@ -28,8 +29,11 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeH func New(ctx context.Context, st store.Store) http.Handler { s := &Service{chi.NewMux(), log.Print, log.Printf, log.Fatal, log.Fatalf} + s.routes() auth.NewService(ctx, s.m, st.UserRepo(), st.TokenClient()) + jam.NewService(ctx, s.m) + return s } From 8d3f583c2d3592c4e52241906de32aa92d4227ff Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 7 Oct 2022 23:20:38 +0100 Subject: [PATCH 175/246] example of a repo that wraps sqlc gen code w/ test --- store/sql/author/author.sql | 34 +++++++++++++ store/sql/author/author.sql.go | 89 ++++++++++++++++++++++++++++++++++ store/sql/author/db.go | 31 ++++++++++++ store/sql/author/models.go | 15 ++++++ store/sql/author/repo.go | 65 +++++++++++++++++++++++++ store/sql/author/repo_test.go | 37 ++++++++++++++ 6 files changed, 271 insertions(+) create mode 100644 store/sql/author/author.sql create mode 100644 store/sql/author/author.sql.go create mode 100644 store/sql/author/db.go create mode 100644 store/sql/author/models.go create mode 100644 store/sql/author/repo.go create mode 100644 store/sql/author/repo_test.go diff --git a/store/sql/author/author.sql b/store/sql/author/author.sql new file mode 100644 index 00000000..c94958b4 --- /dev/null +++ b/store/sql/author/author.sql @@ -0,0 +1,34 @@ +-- Example queries for sqlc +CREATE TEMP TABLE authors ( + id bigserial PRIMARY KEY, + name text NOT NULL, + bio text +); + +-- name: GetAuthor :one +SELECT + * +FROM + authors +WHERE + id = $1 +LIMIT 1; + +-- name: ListAuthors :many +SELECT + * +FROM + authors +ORDER BY + name; + +-- name: CreateAuthor :one +INSERT INTO authors (name, bio) + VALUES ($1, $2) +RETURNING + *; + +-- name: DeleteAuthor :exec +DELETE FROM authors +WHERE id = $1; + diff --git a/store/sql/author/author.sql.go b/store/sql/author/author.sql.go new file mode 100644 index 00000000..422081e2 --- /dev/null +++ b/store/sql/author/author.sql.go @@ -0,0 +1,89 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.15.0 +// source: author.sql + +package author + +import ( + "context" + "database/sql" +) + +const createAuthor = `-- name: CreateAuthor :one +INSERT INTO authors (name, bio) + VALUES ($1, $2) +RETURNING + id, name, bio +` + +type CreateAuthorParams struct { + Name string + Bio sql.NullString +} + +func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) { + row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio) + var i Author + err := row.Scan(&i.ID, &i.Name, &i.Bio) + return i, err +} + +const deleteAuthor = `-- name: DeleteAuthor :exec +DELETE FROM authors +WHERE id = $1 +` + +func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deleteAuthor, id) + return err +} + +const getAuthor = `-- name: GetAuthor :one +SELECT + id, name, bio +FROM + authors +WHERE + id = $1 +LIMIT 1 +` + +func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { + row := q.db.QueryRowContext(ctx, getAuthor, id) + var i Author + err := row.Scan(&i.ID, &i.Name, &i.Bio) + return i, err +} + +const listAuthors = `-- name: ListAuthors :many +SELECT + id, name, bio +FROM + authors +ORDER BY + name +` + +func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) { + rows, err := q.db.QueryContext(ctx, listAuthors) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Author + for rows.Next() { + var i Author + if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/store/sql/author/db.go b/store/sql/author/db.go new file mode 100644 index 00000000..309c7ff0 --- /dev/null +++ b/store/sql/author/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.15.0 + +package author + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/store/sql/author/models.go b/store/sql/author/models.go new file mode 100644 index 00000000..1eb5b8e7 --- /dev/null +++ b/store/sql/author/models.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.15.0 + +package author + +import ( + "database/sql" +) + +type Author struct { + ID int64 + Name string + Bio sql.NullString +} diff --git a/store/sql/author/repo.go b/store/sql/author/repo.go new file mode 100644 index 00000000..4e609df9 --- /dev/null +++ b/store/sql/author/repo.go @@ -0,0 +1,65 @@ +package author + +import ( + "context" + "database/sql" +) + +type AuthorRepo interface { + ListAuthors(ctx context.Context) ([]Author, error) + InsertAuthor(ctx context.Context, a *InternalAuthor) (Author, error) +} + +type Repo struct { + q *Queries +} + +func NewRepo(ctx context.Context, connString string) AuthorRepo { + db, err := sql.Open("postgres", connString) + if err != nil { + panic(err) + } + + // test_authors + _, err = db.ExecContext(context.Background(), ` + CREATE TEMP TABLE authors ( + id bigserial PRIMARY KEY, + name text NOT NULL, + bio text + )`) + + if err != nil { + panic(err) + } + + q := New(db) + + return &Repo{ + q, + } +} + +func (r *Repo) ListAuthors(ctx context.Context) ([]Author, error) { + return r.q.ListAuthors(ctx) +} + +func (r *Repo) InsertAuthor(ctx context.Context, a *InternalAuthor) (Author, error) { + var valid bool + if a.Bio != "" { + valid = true + } + + v := CreateAuthorParams{ + Name: a.Name, + Bio: sql.NullString{String: a.Bio, Valid: valid}, + } + + _, err := r.q.CreateAuthor(ctx, v) + return Author{}, err +} + +// this would be inside the internal package +type InternalAuthor struct { + Name string + Bio string +} diff --git a/store/sql/author/repo_test.go b/store/sql/author/repo_test.go new file mode 100644 index 00000000..19b74519 --- /dev/null +++ b/store/sql/author/repo_test.go @@ -0,0 +1,37 @@ +package author + +import ( + "context" + "testing" + + "github.com/rog-golang-buddies/rmx/internal/is" + + _ "github.com/lib/pq" +) + +var r AuthorRepo + +func init() { + r = NewRepo(context.Background(), `postgres://postgres:postgrespw@localhost:49153?sslmode=disable`) +} + +func TestRepo(t *testing.T) { + t.Parallel() + is := is.New(t) + + t.Run("list all users", func(t *testing.T) { + authors, err := r.ListAuthors(context.Background()) + is.NoErr(err) // query all authors + is.Equal(len(authors), 0) // no authors in database + }) + + t.Run("insert a new author", func(t *testing.T) { + payload := &InternalAuthor{ + Name: "Brian Kernighan", + Bio: "Co-author of The C Programming Language and The Go Programming Language", + } + + _, err := r.InsertAuthor(context.Background(), payload) + is.NoErr(err) // inserting author + }) +} From 41dbc9da3e1d7aba92d31f976625646e00517bd5 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 7 Oct 2022 23:21:24 +0100 Subject: [PATCH 176/246] gen file can live in here --- store/sql/gen.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 store/sql/gen.json diff --git a/store/sql/gen.json b/store/sql/gen.json new file mode 100644 index 00000000..72edaf34 --- /dev/null +++ b/store/sql/gen.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "postgresql", + "schema": "author/author.sql", + "queries": "author/author.sql", + "gen": { + "go": { + "package": "author", + "out": "author" + } + } + } + ] +} \ No newline at end of file From 38e67647d0f4a5dfab07f0531980600b916f0d29 Mon Sep 17 00:00:00 2001 From: adoublef Date: Fri, 7 Oct 2022 23:21:40 +0100 Subject: [PATCH 177/246] cleanup --- cmd/main.go | 54 ------------- go.mod | 12 ++- go.sum | 169 +++++++++++++++++++++++++++++++++++++++- store/user/user.go | 3 +- store/user/user_test.go | 85 ++++++++++++++++++++ 5 files changed, 263 insertions(+), 60 deletions(-) create mode 100644 store/user/user_test.go diff --git a/cmd/main.go b/cmd/main.go index 9cd2c2e4..c36e20be 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -35,57 +35,3 @@ func initCLI() *cli.App { return c } - -// func getEnv(key, fallback string) string { -// if value, ok := os.LookupEnv(key); ok { -// return value -// } -// return fallback -// } - -// func init() { -// // // name of config file (without extension) -// // viper.SetConfigName("config") -// // // REQUIRED if the config file does not have the extension in the name -// // viper.SetConfigType("env") -// // // optionally look for config in the working directory -// // viper.AddConfigPath(".") - -// //// Set Default variables -// // viper.SetDefault("PORT", "8080") - -// // viper.AutomaticEnv() - -// // if err := viper.ReadInConfig(); err != nil { -// // panic(err) -// // } -// } - -// // func LoadConfig(path string) (config Config, err error) { -// // // Read file path -// // viper.AddConfigPath(path) -// // // set config file and path -// // viper.SetConfigName("app") -// // viper.SetConfigType("env") -// // // watching changes in app.env -// // viper.AutomaticEnv() -// // // reading the config file -// // err = viper.ReadInConfig() -// // if err != nil { -// // return -// // } - -// // err = viper.Unmarshal(&config) -// // return -// // } - -// func loadConfig() error { -// _, b, _, _ := runtime.Caller(0) -// basepath := filepath.Join(filepath.Dir(b), "../") -// viper.SetConfigFile(basepath + ".env") -// // viper.AddConfigPath("../") -// viper.SetConfigType("dotenv") -// // viper.SetConfigFile(".env") - -// return viper.ReadInConfig() -// } diff --git a/go.mod b/go.mod index d2072352..a94fdc61 100644 --- a/go.mod +++ b/go.mod @@ -11,12 +11,14 @@ require ( github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 github.com/hyphengolang/prelude v0.0.7 + github.com/jackc/pgconn v1.12.1 + github.com/jackc/pgx/v4 v4.16.1 github.com/lithammer/shortuuid/v4 v4.0.0 github.com/manifoldco/promptui v0.9.0 github.com/pkg/errors v0.9.1 github.com/rs/cors v1.8.2 github.com/urfave/cli/v2 v2.16.3 - golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e + golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 golang.org/x/exp v0.0.0-20220921164117-439092de6870 golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 golang.org/x/term v0.0.0-20220919170432-7a66f970e087 @@ -28,11 +30,18 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/goccy/go-json v0.9.11 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect + github.com/jackc/pgtype v1.11.0 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect ) require ( @@ -40,6 +49,7 @@ require ( github.com/containerd/console v1.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/go-redis/redis/v9 v9.0.0-beta.2 + github.com/jackc/pgx/v5 v5.0.1 github.com/lestrrat-go/jwx/v2 v2.0.6 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.16 // indirect diff --git a/go.sum b/go.sum index 740a3f75..1b779ae5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -18,10 +20,15 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -35,17 +42,81 @@ github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-redis/redis/v9 v9.0.0-beta.2 h1:ZSr84TsnQyKMAg8gnV+oawuQezeJR11/09THcWCQzr4= github.com/go-redis/redis/v9 v9.0.0-beta.2/go.mod h1:Bldcd/M/bm9HbnNPi/LUtYBSD8ttcZYBMupwMXhdU0o= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hyphengolang/prelude v0.0.7 h1:AgLkbASkg/3ioIjr0JinhReTiaeppIaX2sa7vrFwaoc= github.com/hyphengolang/prelude v0.0.7/go.mod h1:obKnO4SQhPfu3gqA9g29nbDRQLNTtmJy02i7KYZGdfM= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= +github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y= +github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs= +github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= +github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= +github.com/jackc/pgx/v5 v5.0.1 h1:JZu9othr7l8so2JMDAGeDUMXqERAuZpovyfl4H50tdg= +github.com/jackc/pgx/v5 v5.0.1/go.mod h1:JBbvW3Hdw77jKl9uJrEDATUZIFM2VFPzRq4RWIhkF4o= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= @@ -59,12 +130,22 @@ github.com/lestrrat-go/jwx/v2 v2.0.6 h1:RlyYNLV892Ed7+FTfj1ROoF6x7WxL965PGTHso/6 github.com/lestrrat-go/jwx/v2 v2.0.6/go.mod h1:aVrGuwEr3cp2Prw6TtQvr8sQxe+84gruID5C9TxT64Q= github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -88,6 +169,7 @@ github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -95,14 +177,33 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -112,16 +213,56 @@ github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSV github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20220921164117-439092de6870 h1:j8b6j9gzSigH28O5SjSpQSSh9lFd6f5D/q0aHjNTulc= golang.org/x/exp v0.0.0-20220921164117-439092de6870/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -131,17 +272,39 @@ golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w= golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/store/user/user.go b/store/user/user.go index 0dd6ebbf..1d67a0ff 100644 --- a/store/user/user.go +++ b/store/user/user.go @@ -17,9 +17,8 @@ type Repo struct { c *sql.DB } -func New(ctx context.Context) internal.UserRepo { +func NewRepo(ctx context.Context) internal.UserRepo { var conn *sql.DB - user.New(conn) return nil } diff --git a/store/user/user_test.go b/store/user/user_test.go new file mode 100644 index 00000000..16e09dad --- /dev/null +++ b/store/user/user_test.go @@ -0,0 +1,85 @@ +package user + +import ( + "context" + "testing" + + "github.com/jackc/pgx/v5" + "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/is" + "github.com/rog-golang-buddies/rmx/internal/suid" +) + +var db internal.UserRepo + +func init() { + ctx := context.WithValue(context.Background(), internal.DevMode, true) + c, err := pgx.Connect(context.Background(), `postgres://postgres:postgrespw@localhost:49153/postgres`) + if err != nil { + panic(err) + } + + db = NewRepo(ctx, c) +} + +func TestSQLRepo(t *testing.T) { + t.Parallel() + + is, ctx := is.New(t), context.Background() + + t.Run("insert two users to database", func(t *testing.T) { + fizz := internal.User{ + ID: suid.NewUUID(), + Email: "fizz@mail.com", + Username: "fizz", + Password: internal.Password("fizz_pw_1").MustHash(), + } + + err := db.Insert(ctx, &fizz) + is.NoErr(err) // insert new user "fizz" + + buzz := internal.User{ + ID: suid.NewUUID(), + Email: "buzz@mail.com", + Username: "buzz", + Password: internal.Password("buzz_pw_1").MustHash(), + } + + err = db.Insert(ctx, &buzz) + is.NoErr(err) // insert new user "buzz" + + us, err := db.SelectMany(ctx) + is.NoErr(err) // select all users + is.Equal(len(us), 2) // should be a length of 2 + }) + + t.Run("reject user with duplicate email/username", func(t *testing.T) { + fizz := internal.User{ + ID: suid.NewUUID(), + Email: "fuzz@mail.com", + Username: "fizz", + Password: internal.Password("fuzz_pw_1").MustHash(), + } + + err := db.Insert(ctx, &fizz) + is.True(err != nil) // duplicate user with username "fizz" + }) + + t.Run("select a user from the database using email/username", func(t *testing.T) { + u, err := db.Select(ctx, "fizz") + is.NoErr(err) // select user where username = "fizz" + is.NoErr(u.Password.Compare("fizz_pw_1")) // valid login + + _, err = db.Select(ctx, internal.Email("buzz@mail.com")) + is.NoErr(err) // select user where email = "buzz@mail.com" + }) + + t.Run("delete user from database, return 1 user in database", func(t *testing.T) { + err := db.Delete(ctx, "fizz") + is.NoErr(err) // delete user where username == "fizz" + + us, err := db.SelectMany(ctx) + is.NoErr(err) // select all users + is.Equal(len(us), 1) // should be a length of 1 + }) +} From 653fe615b6a820fb563a70e7d1cbd77c0ed266c1 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 8 Oct 2022 00:14:04 +0100 Subject: [PATCH 178/246] getting user db setup with tests --- store/sql/user/db.go | 111 +--------- store/sql/user/models.go | 13 +- store/sql/user/repo.go | 199 +++++++++++++++--- .../user_test.go => sql/user/repo_test.go} | 22 +- store/sql/user/user.sql | 51 +++++ store/sql/user/user.sql.go | 145 +++++++++++++ store/sql/user/user_query.sql.go | 150 ------------- store/user/user.go | 135 ------------ 8 files changed, 385 insertions(+), 441 deletions(-) rename store/{user/user_test.go => sql/user/repo_test.go} (78%) create mode 100644 store/sql/user/user.sql create mode 100644 store/sql/user/user.sql.go delete mode 100644 store/sql/user/user_query.sql.go delete mode 100644 store/user/user.go diff --git a/store/sql/user/db.go b/store/sql/user/db.go index ed263c95..00b4c957 100644 --- a/store/sql/user/db.go +++ b/store/sql/user/db.go @@ -7,7 +7,6 @@ package user import ( "context" "database/sql" - "fmt" ) type DBTX interface { @@ -21,118 +20,12 @@ func New(db DBTX) *Queries { return &Queries{db: db} } -func Prepare(ctx context.Context, db DBTX) (*Queries, error) { - q := Queries{db: db} - var err error - if q.createUserStmt, err = db.PrepareContext(ctx, createUser); err != nil { - return nil, fmt.Errorf("error preparing query CreateUser: %w", err) - } - if q.deleteUserStmt, err = db.PrepareContext(ctx, deleteUser); err != nil { - return nil, fmt.Errorf("error preparing query DeleteUser: %w", err) - } - if q.getUserByEmailStmt, err = db.PrepareContext(ctx, getUserByEmail); err != nil { - return nil, fmt.Errorf("error preparing query GetUserByEmail: %w", err) - } - if q.getUserByIDStmt, err = db.PrepareContext(ctx, getUserByID); err != nil { - return nil, fmt.Errorf("error preparing query GetUserByID: %w", err) - } - if q.listUsersStmt, err = db.PrepareContext(ctx, listUsers); err != nil { - return nil, fmt.Errorf("error preparing query ListUsers: %w", err) - } - if q.updateUserStmt, err = db.PrepareContext(ctx, updateUser); err != nil { - return nil, fmt.Errorf("error preparing query UpdateUser: %w", err) - } - return &q, nil -} - -func (q *Queries) Close() error { - var err error - if q.createUserStmt != nil { - if cerr := q.createUserStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing createUserStmt: %w", cerr) - } - } - if q.deleteUserStmt != nil { - if cerr := q.deleteUserStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing deleteUserStmt: %w", cerr) - } - } - if q.getUserByEmailStmt != nil { - if cerr := q.getUserByEmailStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing getUserByEmailStmt: %w", cerr) - } - } - if q.getUserByIDStmt != nil { - if cerr := q.getUserByIDStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing getUserByIDStmt: %w", cerr) - } - } - if q.listUsersStmt != nil { - if cerr := q.listUsersStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing listUsersStmt: %w", cerr) - } - } - if q.updateUserStmt != nil { - if cerr := q.updateUserStmt.Close(); cerr != nil { - err = fmt.Errorf("error closing updateUserStmt: %w", cerr) - } - } - return err -} - -func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (sql.Result, error) { - switch { - case stmt != nil && q.tx != nil: - return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...) - case stmt != nil: - return stmt.ExecContext(ctx, args...) - default: - return q.db.ExecContext(ctx, query, args...) - } -} - -func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) { - switch { - case stmt != nil && q.tx != nil: - return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...) - case stmt != nil: - return stmt.QueryContext(ctx, args...) - default: - return q.db.QueryContext(ctx, query, args...) - } -} - -func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) *sql.Row { - switch { - case stmt != nil && q.tx != nil: - return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...) - case stmt != nil: - return stmt.QueryRowContext(ctx, args...) - default: - return q.db.QueryRowContext(ctx, query, args...) - } -} - type Queries struct { - db DBTX - tx *sql.Tx - createUserStmt *sql.Stmt - deleteUserStmt *sql.Stmt - getUserByEmailStmt *sql.Stmt - getUserByIDStmt *sql.Stmt - listUsersStmt *sql.Stmt - updateUserStmt *sql.Stmt + db DBTX } func (q *Queries) WithTx(tx *sql.Tx) *Queries { return &Queries{ - db: tx, - tx: tx, - createUserStmt: q.createUserStmt, - deleteUserStmt: q.deleteUserStmt, - getUserByEmailStmt: q.getUserByEmailStmt, - getUserByIDStmt: q.getUserByIDStmt, - listUsersStmt: q.listUsersStmt, - updateUserStmt: q.updateUserStmt, + db: tx, } } diff --git a/store/sql/user/models.go b/store/sql/user/models.go index 956d9dd7..db420b68 100644 --- a/store/sql/user/models.go +++ b/store/sql/user/models.go @@ -5,16 +5,13 @@ package user import ( - "database/sql" "time" ) type User struct { - ID string `db:"id" json:"id"` - Username string `db:"username" json:"username"` - Email string `db:"email" json:"email"` - Password string `db:"password" json:"password"` - CreatedAt time.Time `db:"created_at" json:"createdAt"` - UpdatedAt sql.NullTime `db:"updated_at" json:"updatedAt"` - DeletedAt sql.NullTime `db:"deleted_at" json:"deletedAt"` + ID string + Username string + Email string + Password string + CreatedAt time.Time } diff --git a/store/sql/user/repo.go b/store/sql/user/repo.go index 409457e5..a8bc03bf 100644 --- a/store/sql/user/repo.go +++ b/store/sql/user/repo.go @@ -3,59 +3,190 @@ package user import ( "context" "database/sql" - "errors" + "log" + "sync" + "time" + "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/suid" ) -type userRepo struct { - DBConn *sql.DB +type Repo struct { + q *Queries } -var ( - errTodo = errors.New("not yet implemented") - errNotFound = errors.New("user not found") - errExists = errors.New("user already exists") -) +func NewRepo(ctx context.Context, conn *sql.DB) internal.UserRepo { + r := &Repo{New(conn)} + return r +} -func UserRepo(c *sql.DB) *userRepo { - r := &userRepo{ - DBConn: c, - } +func UserRepo(c *sql.DB) *Repo { + r := &Repo{q: New(c)} return r } -func (r *userRepo) ListAll() ([]User, error) { - ctx := context.Background() - q := New(r.DBConn) - return q.ListUsers(ctx) +// We need to setup the config to avoid needing to do this +func (r *Repo) SelectMany(ctx context.Context) ([]internal.User, error) { + us, err := r.q.ListUsers(ctx) + ius := fp.FMap(us, func(u User) internal.User { + return internal.User{ + ID: suid.MustParse(u.ID), Username: u.Username, Email: internal.Email(u.Email), + Password: internal.PasswordHash(u.Password), + } + }) + + return ius, err +} + +func (r *Repo) Select(ctx context.Context, key any) (u *internal.User, err error) { + var v User + switch key := key.(type) { + case suid.UUID: + v, err = r.q.GetUserByID(ctx, key.String()) + case internal.Email: + v, err = r.q.GetUserByEmail(ctx, key.String()) + default: + return nil, internal.ErrInvalidType + } + + u = &internal.User{ + ID: suid.MustParse(v.ID), + Username: v.Username, + Email: internal.Email(v.Email), + Password: internal.PasswordHash(v.Password), + } + + return u, err } -func (r *userRepo) Lookup(uid *suid.UUID) (User, error) { - ctx := context.Background() - q := New(r.DBConn) - return q.GetUserByID(ctx, uid.String()) +func (r *Repo) Insert(ctx context.Context, u *internal.User) error { + v := CreateUserParams{ + Username: u.Username, + Email: u.Email.String(), + Password: u.Password.String(), + CreatedAt: time.Now(), + } + + _, err := r.q.CreateUser(ctx, v) + return err +} + +func (r *Repo) Remove(ctx context.Context, key any) error { + switch key := key.(type) { + case suid.UUID: + return r.q.DeleteUser(ctx, key.String()) + // case internal.Email: + // v, err = r.q.GetUserByEmail(ctx, key.String()) + default: + return internal.ErrInvalidType + } +} + +var DefaultRepo = &repo{ + miu: make(map[suid.UUID]*User), + mei: make(map[string]*User), + log: log.Println, + logf: log.Printf, +} + +type repo struct { + mu sync.Mutex + miu map[suid.UUID]*User + mei map[string]*User + + log func(v ...any) + logf func(format string, v ...any) } -func (r *userRepo) LookupEmail(email string) (User, error) { - ctx := context.Background() - q := New(r.DBConn) - return q.GetUserByEmail(ctx, email) +// Remove implements internal.UserRepo +func (r *repo) Remove(ctx context.Context, key any) error { + panic("unimplemented") } -func (r *userRepo) Insert(u *CreateUserParams) error { - ctx := context.Background() - q := New(r.DBConn) - _, err := q.CreateUser(ctx, u) - if err != nil { - return err +func (r *repo) Insert(ctx context.Context, iu *internal.User) error { + r.mu.Lock() + defer r.mu.Unlock() + + if _, found := r.mei[iu.Email.String()]; found { + return internal.ErrAlreadyExists + } + + u := &User{ + ID: iu.ID.String(), + Username: iu.Username, + Email: iu.Email.String(), + Password: iu.Password.String(), + CreatedAt: time.Now(), } + r.mei[iu.Email.String()], r.miu[iu.ID] = u, u return nil } -func (r *userRepo) Remove(uid *suid.UUID) error { - ctx := context.Background() - q := New(r.DBConn) - return q.DeleteUser(ctx, uid.String()) +func (r *repo) SelectMany(ctx context.Context) ([]internal.User, error) { + panic("not implemented") +} + +func (r *repo) Select(ctx context.Context, key any) (*internal.User, error) { + switch key := key.(type) { + case suid.UUID: + return r.selectUUID(key) + case internal.Email: + return r.selectEmail(key) + case string: + return r.selectUsername(key) + default: + return nil, internal.ErrInvalidType + } +} + +func (r *repo) selectUUID(uid suid.UUID) (*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() + + if u, ok := r.miu[uid]; ok { + return &internal.User{ + ID: suid.MustParse(u.ID), + Username: u.Username, + Email: internal.Email(u.Email), + Password: internal.PasswordHash(u.Password), + }, nil + } + + return nil, internal.ErrNotFound +} + +func (r *repo) selectUsername(username string) (*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() + + for _, u := range r.mei { + if u.Username == username { + return &internal.User{ + ID: suid.MustParse(u.ID), + Username: u.Username, + Email: internal.Email(u.Email), + Password: internal.PasswordHash(u.Password), + }, nil + } + } + + return nil, internal.ErrNotFound +} + +func (r *repo) selectEmail(email internal.Email) (*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() + + if u, ok := r.mei[email.String()]; ok { + return &internal.User{ + ID: suid.MustParse(u.ID), + Username: u.Username, + Email: internal.Email(u.Email), + Password: internal.PasswordHash(u.Password), + }, nil + } + + return nil, internal.ErrNotFound } diff --git a/store/user/user_test.go b/store/sql/user/repo_test.go similarity index 78% rename from store/user/user_test.go rename to store/sql/user/repo_test.go index 16e09dad..f5ba3b21 100644 --- a/store/user/user_test.go +++ b/store/sql/user/repo_test.go @@ -2,24 +2,36 @@ package user import ( "context" + "database/sql" "testing" - "github.com/jackc/pgx/v5" "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/is" "github.com/rog-golang-buddies/rmx/internal/suid" + + _ "github.com/lib/pq" // for testing purposes only as I haven't setup mysql on my device yet ) var db internal.UserRepo func init() { - ctx := context.WithValue(context.Background(), internal.DevMode, true) - c, err := pgx.Connect(context.Background(), `postgres://postgres:postgrespw@localhost:49153/postgres`) + c, err := sql.Open(`postgres`, `postgres://postgres:postgrespw@localhost:49153/postgres?sslmode=disable`) if err != nil { panic(err) } - db = NewRepo(ctx, c) + if _, err := c.Exec(`CREATE TEMP TABLE users ( + id text NOT NULL PRIMARY KEY, + username text NOT NULL, + email text NOT NULL, + password text NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), + UNIQUE (email) + );`); err != nil { + panic(err) + } + + db = NewRepo(context.Background(), c) } func TestSQLRepo(t *testing.T) { @@ -75,7 +87,7 @@ func TestSQLRepo(t *testing.T) { }) t.Run("delete user from database, return 1 user in database", func(t *testing.T) { - err := db.Delete(ctx, "fizz") + err := db.Remove(ctx, "fizz") is.NoErr(err) // delete user where username == "fizz" us, err := db.SelectMany(ctx) diff --git a/store/sql/user/user.sql b/store/sql/user/user.sql new file mode 100644 index 00000000..9b878c7e --- /dev/null +++ b/store/sql/user/user.sql @@ -0,0 +1,51 @@ +CREATE TABLE users ( + id text NOT NULL PRIMARY KEY, + username text NOT NULL, + email text NOT NULL, + password text NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), + UNIQUE (email) +); + +-- name: GetUserByID :one +SELECT + * +FROM + users +WHERE + id = $1 +LIMIT 1; + +-- name: GetUserByEmail :one +SELECT + * +FROM + users +WHERE + email = $1 +LIMIT 1; + +-- name: ListUsers :many +SELECT + * +FROM + users +ORDER BY + id; + +-- name: CreateUser :execresult +INSERT INTO users (username, email, PASSWORD, created_at) + VALUES ($1, $2, $3, $4); + +-- name: UpdateUser :execresult +UPDATE + users +SET + username = $1 +WHERE + id = $2; + +-- name: DeleteUser :exec +DELETE FROM users +WHERE id = $1; + diff --git a/store/sql/user/user.sql.go b/store/sql/user/user.sql.go new file mode 100644 index 00000000..dbd438e0 --- /dev/null +++ b/store/sql/user/user.sql.go @@ -0,0 +1,145 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.15.0 +// source: user.sql + +package user + +import ( + "context" + "database/sql" + "time" +) + +const createUser = `-- name: CreateUser :execresult +INSERT INTO users (username, email, PASSWORD, created_at) + VALUES ($1, $2, $3, $4) +` + +type CreateUserParams struct { + Username string + Email string + Password string + CreatedAt time.Time +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (sql.Result, error) { + return q.db.ExecContext(ctx, createUser, + arg.Username, + arg.Email, + arg.Password, + arg.CreatedAt, + ) +} + +const deleteUser = `-- name: DeleteUser :exec +DELETE FROM users +WHERE id = $1 +` + +func (q *Queries) DeleteUser(ctx context.Context, id string) error { + _, err := q.db.ExecContext(ctx, deleteUser, id) + return err +} + +const getUserByEmail = `-- name: GetUserByEmail :one +SELECT + id, username, email, password, created_at +FROM + users +WHERE + email = $1 +LIMIT 1 +` + +func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByEmail, email) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.Password, + &i.CreatedAt, + ) + return i, err +} + +const getUserByID = `-- name: GetUserByID :one +SELECT + id, username, email, password, created_at +FROM + users +WHERE + id = $1 +LIMIT 1 +` + +func (q *Queries) GetUserByID(ctx context.Context, id string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByID, id) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.Password, + &i.CreatedAt, + ) + return i, err +} + +const listUsers = `-- name: ListUsers :many +SELECT + id, username, email, password, created_at +FROM + users +ORDER BY + id +` + +func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { + rows, err := q.db.QueryContext(ctx, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.Password, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateUser = `-- name: UpdateUser :execresult +UPDATE + users +SET + username = $1 +WHERE + id = $2 +` + +type UpdateUserParams struct { + Username string + ID string +} + +func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (sql.Result, error) { + return q.db.ExecContext(ctx, updateUser, arg.Username, arg.ID) +} diff --git a/store/sql/user/user_query.sql.go b/store/sql/user/user_query.sql.go deleted file mode 100644 index cfa6637c..00000000 --- a/store/sql/user/user_query.sql.go +++ /dev/null @@ -1,150 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.15.0 -// source: user_query.sql - -package user - -import ( - "context" - "database/sql" - "time" -) - -const createUser = `-- name: CreateUser :execresult -INSERT INTO users ( - username, - email, - password, - created_at, - updated_at, - deleted_at - ) -VALUES (?, ?, ?, ?, ?, ?) -` - -type CreateUserParams struct { - Username string `db:"username" json:"username"` - Email string `db:"email" json:"email"` - Password string `db:"password" json:"password"` - CreatedAt time.Time `db:"created_at" json:"createdAt"` - UpdatedAt sql.NullTime `db:"updated_at" json:"updatedAt"` - DeletedAt sql.NullTime `db:"deleted_at" json:"deletedAt"` -} - -func (q *Queries) CreateUser(ctx context.Context, arg *CreateUserParams) (sql.Result, error) { - return q.exec(ctx, q.createUserStmt, createUser, - arg.Username, - arg.Email, - arg.Password, - arg.CreatedAt, - arg.UpdatedAt, - arg.DeletedAt, - ) -} - -const deleteUser = `-- name: DeleteUser :exec -DELETE FROM users -WHERE id = ? -` - -func (q *Queries) DeleteUser(ctx context.Context, id string) error { - _, err := q.exec(ctx, q.deleteUserStmt, deleteUser, id) - return err -} - -const getUserByEmail = `-- name: GetUserByEmail :one -SELECT id, username, email, password, created_at, updated_at, deleted_at -FROM users -WHERE email = ? -LIMIT 1 -` - -func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { - row := q.queryRow(ctx, q.getUserByEmailStmt, getUserByEmail, email) - var i User - err := row.Scan( - &i.ID, - &i.Username, - &i.Email, - &i.Password, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - ) - return i, err -} - -const getUserByID = `-- name: GetUserByID :one -SELECT id, username, email, password, created_at, updated_at, deleted_at -FROM users -WHERE id = ? -LIMIT 1 -` - -func (q *Queries) GetUserByID(ctx context.Context, id string) (User, error) { - row := q.queryRow(ctx, q.getUserByIDStmt, getUserByID, id) - var i User - err := row.Scan( - &i.ID, - &i.Username, - &i.Email, - &i.Password, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - ) - return i, err -} - -const listUsers = `-- name: ListUsers :many -SELECT id, username, email, password, created_at, updated_at, deleted_at -FROM users -ORDER BY id -` - -func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { - rows, err := q.query(ctx, q.listUsersStmt, listUsers) - if err != nil { - return nil, err - } - defer rows.Close() - items := []User{} - for rows.Next() { - var i User - if err := rows.Scan( - &i.ID, - &i.Username, - &i.Email, - &i.Password, - &i.CreatedAt, - &i.UpdatedAt, - &i.DeletedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateUser = `-- name: UpdateUser :execresult -UPDATE users - SET username = ? - WHERE id = ? -` - -type UpdateUserParams struct { - Username string `db:"username" json:"username"` - ID string `db:"id" json:"id"` -} - -func (q *Queries) UpdateUser(ctx context.Context, arg *UpdateUserParams) (sql.Result, error) { - return q.exec(ctx, q.updateUserStmt, updateUser, arg.Username, arg.ID) -} diff --git a/store/user/user.go b/store/user/user.go deleted file mode 100644 index 1d67a0ff..00000000 --- a/store/user/user.go +++ /dev/null @@ -1,135 +0,0 @@ -package user - -import ( - "context" - "database/sql" - "log" - "sync" - "time" - - "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/suid" - - "github.com/rog-golang-buddies/rmx/store/sql/user" -) - -type Repo struct { - c *sql.DB -} - -func NewRepo(ctx context.Context) internal.UserRepo { - var conn *sql.DB - user.New(conn) - return nil -} - -type User struct { - ID suid.UUID - Username string - Email internal.Email - Password internal.PasswordHash - CreatedAt time.Time -} - -var DefaultRepo = &repo{ - miu: make(map[suid.UUID]*User), - mei: make(map[string]*User), - log: log.Println, - logf: log.Printf, -} - -type repo struct { - mu sync.Mutex - miu map[suid.UUID]*User - mei map[string]*User - - log func(v ...any) - logf func(format string, v ...any) -} - -// ListAll implements internal.UserRepo -func (r *repo) ListAll() ([]internal.User, error) { - panic("unimplemented") -} - -// Remove implements internal.UserRepo -func (r *repo) Remove(uid *suid.UUID) error { - panic("unimplemented") -} - -func (r *repo) Insert(ctx context.Context, iu *internal.User) error { - r.mu.Lock() - defer r.mu.Unlock() - - if _, found := r.mei[iu.Email.String()]; found { - return internal.ErrAlreadyExists - } - - u := &User{iu.ID, iu.Username, iu.Email, iu.Password, time.Now()} - r.mei[iu.Email.String()], r.miu[iu.ID] = u, u - - return nil -} - -func (r *repo) Select(ctx context.Context, key any) (*internal.User, error) { - switch key := key.(type) { - case suid.UUID: - return r.selectUUID(key) - case internal.Email: - return r.selectEmail(key) - case string: - return r.selectUsername(key) - default: - return nil, internal.ErrInvalidType - } -} - -func (r *repo) selectUUID(uid suid.UUID) (*internal.User, error) { - r.mu.Lock() - defer r.mu.Unlock() - - if u, ok := r.miu[uid]; ok { - return &internal.User{ - ID: u.ID, - Username: u.Username, - Email: internal.Email(u.Email), - Password: u.Password, - }, nil - } - - return nil, internal.ErrNotFound -} - -func (r *repo) selectUsername(username string) (*internal.User, error) { - r.mu.Lock() - defer r.mu.Unlock() - - for _, u := range r.mei { - if u.Username == username { - return &internal.User{ - ID: u.ID, - Username: u.Username, - Email: internal.Email(u.Email), - Password: u.Password, - }, nil - } - } - - return nil, internal.ErrNotFound -} - -func (r *repo) selectEmail(email internal.Email) (*internal.User, error) { - r.mu.Lock() - defer r.mu.Unlock() - - if u, ok := r.mei[email.String()]; ok { - return &internal.User{ - ID: u.ID, - Username: u.Username, - Email: internal.Email(u.Email), - Password: u.Password, - }, nil - } - - return nil, internal.ErrNotFound -} From 051a71a7739a2560612bbf9e877f51ec5e16ff93 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 8 Oct 2022 00:14:23 +0100 Subject: [PATCH 179/246] dummy, will be deleted once user is setup --- store/sql/author/repo.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/store/sql/author/repo.go b/store/sql/author/repo.go index 4e609df9..79f128b7 100644 --- a/store/sql/author/repo.go +++ b/store/sql/author/repo.go @@ -5,6 +5,11 @@ import ( "database/sql" ) +const ( + psql = "postgres" + mysql = "mysql" +) + type AuthorRepo interface { ListAuthors(ctx context.Context) ([]Author, error) InsertAuthor(ctx context.Context, a *InternalAuthor) (Author, error) @@ -15,7 +20,7 @@ type Repo struct { } func NewRepo(ctx context.Context, connString string) AuthorRepo { - db, err := sql.Open("postgres", connString) + db, err := sql.Open(psql, connString) if err != nil { panic(err) } From c0f55906a35ac20c471ab47953e0ec7f8ed9d0f4 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 8 Oct 2022 00:14:35 +0100 Subject: [PATCH 180/246] interface fix --- service/auth/service_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/auth/service_test.go b/service/auth/service_test.go index d0786ae7..4c95980f 100644 --- a/service/auth/service_test.go +++ b/service/auth/service_test.go @@ -12,7 +12,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/rog-golang-buddies/rmx/internal/is" "github.com/rog-golang-buddies/rmx/store/auth" - "github.com/rog-golang-buddies/rmx/store/user" + "github.com/rog-golang-buddies/rmx/store/sql/user" ) const applicationJson = "application/json" From 7438ef8e94c3c0b233c39f2e85022d90c41fe8e5 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 8 Oct 2022 00:14:41 +0100 Subject: [PATCH 181/246] interface fix --- internal/internal.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/internal.go b/internal/internal.go index 71e3e5db..28fea395 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -119,13 +119,13 @@ type UserRepo interface { } type RUserRepo interface { - ListAll() ([]User, error) + SelectMany(ctx context.Context) ([]User, error) Select(ctx context.Context, key any) (*User, error) } type WUserRepo interface { Insert(ctx context.Context, u *User) error - Remove(uid *suid.UUID) error + Remove(ctx context.Context, key any) error } // Custom user type required From a2ecc14248c4fa52d532e00f97453b0343749b8d Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 8 Oct 2022 00:15:00 +0100 Subject: [PATCH 182/246] config inside store/sql package --- store/sql/gen.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/store/sql/gen.json b/store/sql/gen.json index 72edaf34..769b9df3 100644 --- a/store/sql/gen.json +++ b/store/sql/gen.json @@ -11,6 +11,17 @@ "out": "author" } } + }, + { + "engine": "postgresql", + "schema": "user/user.sql", + "queries": "user/user.sql", + "gen": { + "go": { + "package": "user", + "out": "user" + } + } } ] } \ No newline at end of file From e6164e25dc9bb0a8b5579a38532338a4991c98b0 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 8 Oct 2022 00:15:06 +0100 Subject: [PATCH 183/246] got mod tidy --- go.mod | 12 ++--- go.sum | 160 +++------------------------------------------------------ 2 files changed, 10 insertions(+), 162 deletions(-) diff --git a/go.mod b/go.mod index a94fdc61..c3a382a2 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,7 @@ require ( github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 github.com/hyphengolang/prelude v0.0.7 - github.com/jackc/pgconn v1.12.1 - github.com/jackc/pgx/v4 v4.16.1 + github.com/lib/pq v1.10.2 github.com/lithammer/shortuuid/v4 v4.0.0 github.com/manifoldco/promptui v0.9.0 github.com/pkg/errors v0.9.1 @@ -30,18 +29,14 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/goccy/go-json v0.9.11 // indirect - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgio v1.0.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/pgtype v1.11.0 // indirect + github.com/kr/pretty v0.3.0 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) require ( @@ -49,7 +44,6 @@ require ( github.com/containerd/console v1.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/go-redis/redis/v9 v9.0.0-beta.2 - github.com/jackc/pgx/v5 v5.0.1 github.com/lestrrat-go/jwx/v2 v2.0.6 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.16 // indirect diff --git a/go.sum b/go.sum index 1b779ae5..5a468472 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -20,15 +18,11 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -42,81 +36,25 @@ github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-redis/redis/v9 v9.0.0-beta.2 h1:ZSr84TsnQyKMAg8gnV+oawuQezeJR11/09THcWCQzr4= github.com/go-redis/redis/v9 v9.0.0-beta.2/go.mod h1:Bldcd/M/bm9HbnNPi/LUtYBSD8ttcZYBMupwMXhdU0o= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hyphengolang/prelude v0.0.7 h1:AgLkbASkg/3ioIjr0JinhReTiaeppIaX2sa7vrFwaoc= github.com/hyphengolang/prelude v0.0.7/go.mod h1:obKnO4SQhPfu3gqA9g29nbDRQLNTtmJy02i7KYZGdfM= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= -github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y= -github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs= -github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= -github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= -github.com/jackc/pgx/v5 v5.0.1 h1:JZu9othr7l8so2JMDAGeDUMXqERAuZpovyfl4H50tdg= -github.com/jackc/pgx/v5 v5.0.1/go.mod h1:JBbvW3Hdw77jKl9uJrEDATUZIFM2VFPzRq4RWIhkF4o= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= @@ -130,9 +68,6 @@ github.com/lestrrat-go/jwx/v2 v2.0.6 h1:RlyYNLV892Ed7+FTfj1ROoF6x7WxL965PGTHso/6 github.com/lestrrat-go/jwx/v2 v2.0.6/go.mod h1:aVrGuwEr3cp2Prw6TtQvr8sQxe+84gruID5C9TxT64Q= github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= @@ -141,11 +76,6 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -169,7 +99,6 @@ github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -177,33 +106,17 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -213,56 +126,16 @@ github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSV github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20220921164117-439092de6870 h1:j8b6j9gzSigH28O5SjSpQSSh9lFd6f5D/q0aHjNTulc= golang.org/x/exp v0.0.0-20220921164117-439092de6870/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -272,39 +145,20 @@ golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w= golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= From e8754c80088be49df98219d0a584e009f46b9e36 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 8 Oct 2022 07:41:57 +0100 Subject: [PATCH 184/246] mysql & psql generation to see the difference --- store/sql/gen.json | 4 +- store/sql/user/{user.sql => mysql.sql} | 6 +-- store/sql/user/psql.sql | 51 ++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) rename store/sql/user/{user.sql => mysql.sql} (89%) create mode 100644 store/sql/user/psql.sql diff --git a/store/sql/gen.json b/store/sql/gen.json index 769b9df3..de1b65a3 100644 --- a/store/sql/gen.json +++ b/store/sql/gen.json @@ -14,8 +14,8 @@ }, { "engine": "postgresql", - "schema": "user/user.sql", - "queries": "user/user.sql", + "schema": "user/psql.sql", + "queries": "user/psql.sql", "gen": { "go": { "package": "user", diff --git a/store/sql/user/user.sql b/store/sql/user/mysql.sql similarity index 89% rename from store/sql/user/user.sql rename to store/sql/user/mysql.sql index 9b878c7e..ed01be6c 100644 --- a/store/sql/user/user.sql +++ b/store/sql/user/mysql.sql @@ -1,4 +1,4 @@ -CREATE TABLE users ( +CREATE TABLE "users" ( id text NOT NULL PRIMARY KEY, username text NOT NULL, email text NOT NULL, @@ -7,7 +7,7 @@ CREATE TABLE users ( UNIQUE (email) ); --- name: GetUserByID :one +-- name: SelectByID :one SELECT * FROM @@ -16,7 +16,7 @@ WHERE id = $1 LIMIT 1; --- name: GetUserByEmail :one +-- name: SelectByEmail :one SELECT * FROM diff --git a/store/sql/user/psql.sql b/store/sql/user/psql.sql new file mode 100644 index 00000000..ed01be6c --- /dev/null +++ b/store/sql/user/psql.sql @@ -0,0 +1,51 @@ +CREATE TABLE "users" ( + id text NOT NULL PRIMARY KEY, + username text NOT NULL, + email text NOT NULL, + password text NOT NULL, + created_at timestamp NOT NULL DEFAULT NOW(), + UNIQUE (email) +); + +-- name: SelectByID :one +SELECT + * +FROM + users +WHERE + id = $1 +LIMIT 1; + +-- name: SelectByEmail :one +SELECT + * +FROM + users +WHERE + email = $1 +LIMIT 1; + +-- name: ListUsers :many +SELECT + * +FROM + users +ORDER BY + id; + +-- name: CreateUser :execresult +INSERT INTO users (username, email, PASSWORD, created_at) + VALUES ($1, $2, $3, $4); + +-- name: UpdateUser :execresult +UPDATE + users +SET + username = $1 +WHERE + id = $2; + +-- name: DeleteUser :exec +DELETE FROM users +WHERE id = $1; + From 2a47a6f2a9dcc05b55fcbe34c9ebe6f906bccf90 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 8 Oct 2022 07:48:11 +0100 Subject: [PATCH 185/246] store//sql/user/mysql.sql in mysql format again --- store/sql/user/models.go | 12 +-- store/sql/user/mysql.sql | 22 +++--- store/sql/user/psql.sql | 15 ++-- store/sql/user/psql.sql.go | 146 +++++++++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+), 23 deletions(-) create mode 100644 store/sql/user/psql.sql.go diff --git a/store/sql/user/models.go b/store/sql/user/models.go index db420b68..e564b652 100644 --- a/store/sql/user/models.go +++ b/store/sql/user/models.go @@ -5,13 +5,15 @@ package user import ( - "time" + "database/sql" + + "github.com/google/uuid" ) type User struct { - ID string + ID uuid.UUID Username string - Email string - Password string - CreatedAt time.Time + Email interface{} + Password interface{} + CreatedAt sql.NullTime } diff --git a/store/sql/user/mysql.sql b/store/sql/user/mysql.sql index ed01be6c..16a73849 100644 --- a/store/sql/user/mysql.sql +++ b/store/sql/user/mysql.sql @@ -1,28 +1,30 @@ -CREATE TABLE "users" ( +CREATE TABLE users ( id text NOT NULL PRIMARY KEY, username text NOT NULL, email text NOT NULL, password text NOT NULL, created_at timestamp NOT NULL DEFAULT NOW(), + updated_at timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + deleted_at timestamp NULL DEFAULT NULL, UNIQUE (email) ); --- name: SelectByID :one +-- name: GetUserByID :one SELECT * FROM users WHERE - id = $1 + id = ? LIMIT 1; --- name: SelectByEmail :one +-- name: GetUserByEmail :one SELECT * FROM users WHERE - email = $1 + email = ? LIMIT 1; -- name: ListUsers :many @@ -34,18 +36,18 @@ ORDER BY id; -- name: CreateUser :execresult -INSERT INTO users (username, email, PASSWORD, created_at) - VALUES ($1, $2, $3, $4); +INSERT INTO users (username, email, PASSWORD, created_at, updated_at, deleted_at) + VALUES (?, ?, ?, ?, ?, ?); -- name: UpdateUser :execresult UPDATE users SET - username = $1 + username = ? WHERE - id = $2; + id = ?; -- name: DeleteUser :exec DELETE FROM users -WHERE id = $1; +WHERE id = ?; diff --git a/store/sql/user/psql.sql b/store/sql/user/psql.sql index ed01be6c..e7ee1350 100644 --- a/store/sql/user/psql.sql +++ b/store/sql/user/psql.sql @@ -1,10 +1,9 @@ -CREATE TABLE "users" ( - id text NOT NULL PRIMARY KEY, - username text NOT NULL, - email text NOT NULL, - password text NOT NULL, - created_at timestamp NOT NULL DEFAULT NOW(), - UNIQUE (email) +CREATE TEMP TABLE IF NOT EXISTS "users" ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (), + username text UNIQUE NOT NULL CHECK (username <> ''), + email citext UNIQUE NOT NULL CHECK (email ~ '^[a-zA-Z0-9.!#$%&’*+/=?^_\x60{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$'), + PASSWORD citext NOT NULL CHECK (PASSWORD <> ''), + created_at timestamp DEFAULT now() ); -- name: SelectByID :one @@ -25,7 +24,7 @@ WHERE email = $1 LIMIT 1; --- name: ListUsers :many +-- name: SelectMany :many SELECT * FROM diff --git a/store/sql/user/psql.sql.go b/store/sql/user/psql.sql.go new file mode 100644 index 00000000..87461086 --- /dev/null +++ b/store/sql/user/psql.sql.go @@ -0,0 +1,146 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.15.0 +// source: psql.sql + +package user + +import ( + "context" + "database/sql" + + "github.com/google/uuid" +) + +const createUser = `-- name: CreateUser :execresult +INSERT INTO users (username, email, PASSWORD, created_at) + VALUES ($1, $2, $3, $4) +` + +type CreateUserParams struct { + Username string + Email interface{} + Password interface{} + CreatedAt sql.NullTime +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (sql.Result, error) { + return q.db.ExecContext(ctx, createUser, + arg.Username, + arg.Email, + arg.Password, + arg.CreatedAt, + ) +} + +const deleteUser = `-- name: DeleteUser :exec +DELETE FROM users +WHERE id = $1 +` + +func (q *Queries) DeleteUser(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteUser, id) + return err +} + +const listUsers = `-- name: ListUsers :many +SELECT + id, username, email, password, created_at +FROM + users +ORDER BY + id +` + +func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { + rows, err := q.db.QueryContext(ctx, listUsers) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.Password, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const selectByEmail = `-- name: SelectByEmail :one +SELECT + id, username, email, password, created_at +FROM + users +WHERE + email = $1 +LIMIT 1 +` + +func (q *Queries) SelectByEmail(ctx context.Context, email interface{}) (User, error) { + row := q.db.QueryRowContext(ctx, selectByEmail, email) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.Password, + &i.CreatedAt, + ) + return i, err +} + +const selectByID = `-- name: SelectByID :one +SELECT + id, username, email, password, created_at +FROM + users +WHERE + id = $1 +LIMIT 1 +` + +func (q *Queries) SelectByID(ctx context.Context, id uuid.UUID) (User, error) { + row := q.db.QueryRowContext(ctx, selectByID, id) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Email, + &i.Password, + &i.CreatedAt, + ) + return i, err +} + +const updateUser = `-- name: UpdateUser :execresult +UPDATE + users +SET + username = $1 +WHERE + id = $2 +` + +type UpdateUserParams struct { + Username string + ID uuid.UUID +} + +func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (sql.Result, error) { + return q.db.ExecContext(ctx, updateUser, arg.Username, arg.ID) +} From 01df7aa8735f2ba6744f2ef143863adc27f81dc6 Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 8 Oct 2022 09:22:08 +0100 Subject: [PATCH 186/246] auto gen files deleted for time being --- store/sql/user/db.go | 31 -------- store/sql/user/models.go | 19 ----- store/sql/user/psql.sql.go | 146 ------------------------------------- store/sql/user/user.sql.go | 145 ------------------------------------ 4 files changed, 341 deletions(-) delete mode 100644 store/sql/user/db.go delete mode 100644 store/sql/user/models.go delete mode 100644 store/sql/user/psql.sql.go delete mode 100644 store/sql/user/user.sql.go diff --git a/store/sql/user/db.go b/store/sql/user/db.go deleted file mode 100644 index 00b4c957..00000000 --- a/store/sql/user/db.go +++ /dev/null @@ -1,31 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.15.0 - -package user - -import ( - "context" - "database/sql" -) - -type DBTX interface { - ExecContext(context.Context, string, ...interface{}) (sql.Result, error) - PrepareContext(context.Context, string) (*sql.Stmt, error) - QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) - QueryRowContext(context.Context, string, ...interface{}) *sql.Row -} - -func New(db DBTX) *Queries { - return &Queries{db: db} -} - -type Queries struct { - db DBTX -} - -func (q *Queries) WithTx(tx *sql.Tx) *Queries { - return &Queries{ - db: tx, - } -} diff --git a/store/sql/user/models.go b/store/sql/user/models.go deleted file mode 100644 index e564b652..00000000 --- a/store/sql/user/models.go +++ /dev/null @@ -1,19 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.15.0 - -package user - -import ( - "database/sql" - - "github.com/google/uuid" -) - -type User struct { - ID uuid.UUID - Username string - Email interface{} - Password interface{} - CreatedAt sql.NullTime -} diff --git a/store/sql/user/psql.sql.go b/store/sql/user/psql.sql.go deleted file mode 100644 index 87461086..00000000 --- a/store/sql/user/psql.sql.go +++ /dev/null @@ -1,146 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.15.0 -// source: psql.sql - -package user - -import ( - "context" - "database/sql" - - "github.com/google/uuid" -) - -const createUser = `-- name: CreateUser :execresult -INSERT INTO users (username, email, PASSWORD, created_at) - VALUES ($1, $2, $3, $4) -` - -type CreateUserParams struct { - Username string - Email interface{} - Password interface{} - CreatedAt sql.NullTime -} - -func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (sql.Result, error) { - return q.db.ExecContext(ctx, createUser, - arg.Username, - arg.Email, - arg.Password, - arg.CreatedAt, - ) -} - -const deleteUser = `-- name: DeleteUser :exec -DELETE FROM users -WHERE id = $1 -` - -func (q *Queries) DeleteUser(ctx context.Context, id uuid.UUID) error { - _, err := q.db.ExecContext(ctx, deleteUser, id) - return err -} - -const listUsers = `-- name: ListUsers :many -SELECT - id, username, email, password, created_at -FROM - users -ORDER BY - id -` - -func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { - rows, err := q.db.QueryContext(ctx, listUsers) - if err != nil { - return nil, err - } - defer rows.Close() - var items []User - for rows.Next() { - var i User - if err := rows.Scan( - &i.ID, - &i.Username, - &i.Email, - &i.Password, - &i.CreatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const selectByEmail = `-- name: SelectByEmail :one -SELECT - id, username, email, password, created_at -FROM - users -WHERE - email = $1 -LIMIT 1 -` - -func (q *Queries) SelectByEmail(ctx context.Context, email interface{}) (User, error) { - row := q.db.QueryRowContext(ctx, selectByEmail, email) - var i User - err := row.Scan( - &i.ID, - &i.Username, - &i.Email, - &i.Password, - &i.CreatedAt, - ) - return i, err -} - -const selectByID = `-- name: SelectByID :one -SELECT - id, username, email, password, created_at -FROM - users -WHERE - id = $1 -LIMIT 1 -` - -func (q *Queries) SelectByID(ctx context.Context, id uuid.UUID) (User, error) { - row := q.db.QueryRowContext(ctx, selectByID, id) - var i User - err := row.Scan( - &i.ID, - &i.Username, - &i.Email, - &i.Password, - &i.CreatedAt, - ) - return i, err -} - -const updateUser = `-- name: UpdateUser :execresult -UPDATE - users -SET - username = $1 -WHERE - id = $2 -` - -type UpdateUserParams struct { - Username string - ID uuid.UUID -} - -func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (sql.Result, error) { - return q.db.ExecContext(ctx, updateUser, arg.Username, arg.ID) -} diff --git a/store/sql/user/user.sql.go b/store/sql/user/user.sql.go deleted file mode 100644 index dbd438e0..00000000 --- a/store/sql/user/user.sql.go +++ /dev/null @@ -1,145 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.15.0 -// source: user.sql - -package user - -import ( - "context" - "database/sql" - "time" -) - -const createUser = `-- name: CreateUser :execresult -INSERT INTO users (username, email, PASSWORD, created_at) - VALUES ($1, $2, $3, $4) -` - -type CreateUserParams struct { - Username string - Email string - Password string - CreatedAt time.Time -} - -func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (sql.Result, error) { - return q.db.ExecContext(ctx, createUser, - arg.Username, - arg.Email, - arg.Password, - arg.CreatedAt, - ) -} - -const deleteUser = `-- name: DeleteUser :exec -DELETE FROM users -WHERE id = $1 -` - -func (q *Queries) DeleteUser(ctx context.Context, id string) error { - _, err := q.db.ExecContext(ctx, deleteUser, id) - return err -} - -const getUserByEmail = `-- name: GetUserByEmail :one -SELECT - id, username, email, password, created_at -FROM - users -WHERE - email = $1 -LIMIT 1 -` - -func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { - row := q.db.QueryRowContext(ctx, getUserByEmail, email) - var i User - err := row.Scan( - &i.ID, - &i.Username, - &i.Email, - &i.Password, - &i.CreatedAt, - ) - return i, err -} - -const getUserByID = `-- name: GetUserByID :one -SELECT - id, username, email, password, created_at -FROM - users -WHERE - id = $1 -LIMIT 1 -` - -func (q *Queries) GetUserByID(ctx context.Context, id string) (User, error) { - row := q.db.QueryRowContext(ctx, getUserByID, id) - var i User - err := row.Scan( - &i.ID, - &i.Username, - &i.Email, - &i.Password, - &i.CreatedAt, - ) - return i, err -} - -const listUsers = `-- name: ListUsers :many -SELECT - id, username, email, password, created_at -FROM - users -ORDER BY - id -` - -func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { - rows, err := q.db.QueryContext(ctx, listUsers) - if err != nil { - return nil, err - } - defer rows.Close() - var items []User - for rows.Next() { - var i User - if err := rows.Scan( - &i.ID, - &i.Username, - &i.Email, - &i.Password, - &i.CreatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const updateUser = `-- name: UpdateUser :execresult -UPDATE - users -SET - username = $1 -WHERE - id = $2 -` - -type UpdateUserParams struct { - Username string - ID string -} - -func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (sql.Result, error) { - return q.db.ExecContext(ctx, updateUser, arg.Username, arg.ID) -} From a92c2a61ef3113f557547fc6af4dd3f4be8b7b7c Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 8 Oct 2022 09:22:34 +0100 Subject: [PATCH 187/246] internal.UserRepo updated --- internal/internal.go | 11 ++- store/sql/user/repo.go | 172 ++++++++++++++++++++++++++---------- store/sql/user/repo_test.go | 37 ++++---- 3 files changed, 155 insertions(+), 65 deletions(-) diff --git a/internal/internal.go b/internal/internal.go index 28fea395..4c4be58e 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -119,13 +119,22 @@ type UserRepo interface { } type RUserRepo interface { + // Returns an array of users subject to any filter + // conditions that are required SelectMany(ctx context.Context) ([]User, error) + // Returns a user form the database, the "key" + // can be either the "id", "email" or "username" + // as these are all given unique values Select(ctx context.Context, key any) (*User, error) } type WUserRepo interface { + // Insert a new user to the database Insert(ctx context.Context, u *User) error - Remove(ctx context.Context, key any) error + + // Performs a "hard" delete from database + // Restricted to admin only + Delete(ctx context.Context, key any) error } // Custom user type required diff --git a/store/sql/user/repo.go b/store/sql/user/repo.go index a8bc03bf..7edd589a 100644 --- a/store/sql/user/repo.go +++ b/store/sql/user/repo.go @@ -2,85 +2,159 @@ package user import ( "context" - "database/sql" "log" "sync" "time" + "github.com/jackc/pgx/v5" "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/suid" ) +/* +CREATE TABLE users ( + + id text NOT NULL PRIMARY KEY, + username text NOT NULL, + email text NOT NULL, + password text NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL DEFAULT NULL, + UNIQUE (email) + +); +*/ +type User struct { + ID suid.UUID + Username string + Email internal.Email + Password internal.PasswordHash + CreatedAt time.Time + // UpdatedAt *time.Time + // DeletedAt *time.Time +} + type Repo struct { - q *Queries + ctx context.Context + + // connection is using pgx until SQLC is sorted + c *pgx.Conn } -func NewRepo(ctx context.Context, conn *sql.DB) internal.UserRepo { - r := &Repo{New(conn)} - return r +func (r *Repo) Context() context.Context { + if r.ctx != nil { + return r.ctx + } + return context.Background() } -func UserRepo(c *sql.DB) *Repo { - r := &Repo{q: New(c)} +func (r *Repo) Close(ctx context.Context) error { return r.c.Close(ctx) } + +func NewRepo(ctx context.Context, conn *pgx.Conn) internal.UserRepo { + r := &Repo{ctx, conn} return r } +func (r *Repo) Insert(ctx context.Context, iu *internal.User) error { + qry := `insert into "user" (id, email, username, password) values ($1, $2, $3, $4)` + _, err := r.c.Exec(context.Background(), qry, iu.ID, iu.Email, iu.Username, iu.Password.String()) + return err +} + // We need to setup the config to avoid needing to do this func (r *Repo) SelectMany(ctx context.Context) ([]internal.User, error) { - us, err := r.q.ListUsers(ctx) - ius := fp.FMap(us, func(u User) internal.User { - return internal.User{ - ID: suid.MustParse(u.ID), Username: u.Username, Email: internal.Email(u.Email), - Password: internal.PasswordHash(u.Password), + qry := `select id, email, username, password from "user" order by id` + + row, err := r.c.Query(context.Background(), qry) + if err != nil { + return nil, err + } + defer row.Close() + + var ius []internal.User + for row.Next() { + var u User + if err := row.Scan(&u.ID, &u.Email, &u.Username, &u.Password); err != nil { + return nil, err } - }) - return ius, err + iu := internal.User{ID: u.ID, Email: internal.Email(u.Email), Username: u.Username, Password: u.Password} + ius = append(ius, iu) + } + + return ius, nil } -func (r *Repo) Select(ctx context.Context, key any) (u *internal.User, err error) { - var v User +func (r Repo) Select(ctx context.Context, key any) (*internal.User, error) { switch key := key.(type) { case suid.UUID: - v, err = r.q.GetUserByID(ctx, key.String()) + return r.selectUUID(ctx, `select id, email, username, password from "user" where id = $1`, key) case internal.Email: - v, err = r.q.GetUserByEmail(ctx, key.String()) - default: - return nil, internal.ErrInvalidType + return r.selectEmail(ctx, `select id, email, username, password from "user" where email = $1`, key) + case string: + return r.selectUsername(ctx, `select id, email, username, password from "user" where username = $1`, key) } - u = &internal.User{ - ID: suid.MustParse(v.ID), - Username: v.Username, - Email: internal.Email(v.Email), - Password: internal.PasswordHash(v.Password), - } + return nil, internal.ErrInvalidType +} + +func (r Repo) selectUUID(ctx context.Context, qry string, uid suid.UUID) (*internal.User, error) { + row := r.c.QueryRow(ctx, qry, uid) + + var u User + err := row.Scan(&u.ID, &u.Email, &u.Username, &u.Password) - return u, err + iu := &internal.User{ID: u.ID, Email: internal.Email(u.Email), Username: u.Username, Password: u.Password} + return iu, err } -func (r *Repo) Insert(ctx context.Context, u *internal.User) error { - v := CreateUserParams{ - Username: u.Username, - Email: u.Email.String(), - Password: u.Password.String(), - CreatedAt: time.Now(), - } +func (r Repo) selectEmail(ctx context.Context, qry string, email internal.Email) (*internal.User, error) { + row := r.c.QueryRow(ctx, qry, email) - _, err := r.q.CreateUser(ctx, v) - return err + var u User + err := row.Scan(&u.ID, &u.Email, &u.Username, &u.Password) + + iu := &internal.User{ID: u.ID, Email: internal.Email(u.Email), Username: u.Username, Password: u.Password} + return iu, err } -func (r *Repo) Remove(ctx context.Context, key any) error { +func (r Repo) selectUsername(ctx context.Context, qry string, username string) (*internal.User, error) { + row := r.c.QueryRow(ctx, qry, username) + + var u User + err := row.Scan(&u.ID, &u.Email, &u.Username, &u.Password) + + iu := &internal.User{ID: u.ID, Email: internal.Email(u.Email), Username: u.Username, Password: u.Password} + return iu, err +} + +func (r Repo) Delete(ctx context.Context, key any) error { switch key := key.(type) { case suid.UUID: - return r.q.DeleteUser(ctx, key.String()) - // case internal.Email: - // v, err = r.q.GetUserByEmail(ctx, key.String()) - default: - return internal.ErrInvalidType + return r.deleteUUID(ctx, `delete from "user" where id = $1`, key) + case internal.Email: + return r.deleteEmail(ctx, `delete from "user" where email = $1`, key) + case string: + return r.deleteUsername(ctx, `delete from "user" where username = $1`, key) } + + return internal.ErrNotImplemented +} + +func (r Repo) deleteUUID(ctx context.Context, qry string, uid suid.UUID) error { + _, err := r.c.Exec(ctx, qry, uid.String()) + return err +} + +func (r Repo) deleteEmail(ctx context.Context, qry string, email internal.Email) error { + _, err := r.c.Exec(ctx, qry, email.String()) + return err +} + +func (r Repo) deleteUsername(ctx context.Context, qry string, username string) error { + _, err := r.c.Exec(ctx, qry, username) + return err } var DefaultRepo = &repo{ @@ -113,10 +187,10 @@ func (r *repo) Insert(ctx context.Context, iu *internal.User) error { } u := &User{ - ID: iu.ID.String(), + ID: iu.ID, Username: iu.Username, - Email: iu.Email.String(), - Password: iu.Password.String(), + Email: iu.Email, + Password: iu.Password, CreatedAt: time.Now(), } r.mei[iu.Email.String()], r.miu[iu.ID] = u, u @@ -147,7 +221,7 @@ func (r *repo) selectUUID(uid suid.UUID) (*internal.User, error) { if u, ok := r.miu[uid]; ok { return &internal.User{ - ID: suid.MustParse(u.ID), + ID: u.ID, Username: u.Username, Email: internal.Email(u.Email), Password: internal.PasswordHash(u.Password), @@ -164,7 +238,7 @@ func (r *repo) selectUsername(username string) (*internal.User, error) { for _, u := range r.mei { if u.Username == username { return &internal.User{ - ID: suid.MustParse(u.ID), + ID: u.ID, Username: u.Username, Email: internal.Email(u.Email), Password: internal.PasswordHash(u.Password), @@ -181,7 +255,7 @@ func (r *repo) selectEmail(email internal.Email) (*internal.User, error) { if u, ok := r.mei[email.String()]; ok { return &internal.User{ - ID: suid.MustParse(u.ID), + ID: u.ID, Username: u.Username, Email: internal.Email(u.Email), Password: internal.PasswordHash(u.Password), diff --git a/store/sql/user/repo_test.go b/store/sql/user/repo_test.go index f5ba3b21..a177995f 100644 --- a/store/sql/user/repo_test.go +++ b/store/sql/user/repo_test.go @@ -2,31 +2,31 @@ package user import ( "context" - "database/sql" "testing" + "github.com/jackc/pgx/v5" "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/is" "github.com/rog-golang-buddies/rmx/internal/suid" - - _ "github.com/lib/pq" // for testing purposes only as I haven't setup mysql on my device yet ) +/* +https://www.covermymeds.com/main/insights/articles/on-update-timestamps-mysql-vs-postgres/ +*/ var db internal.UserRepo func init() { - c, err := sql.Open(`postgres`, `postgres://postgres:postgrespw@localhost:49153/postgres?sslmode=disable`) + c, err := pgx.Connect(context.Background(), `postgres://postgres:postgrespw@localhost:49153/postgres?sslmode=disable`) if err != nil { panic(err) } - if _, err := c.Exec(`CREATE TEMP TABLE users ( - id text NOT NULL PRIMARY KEY, - username text NOT NULL, - email text NOT NULL, - password text NOT NULL, - created_at timestamp NOT NULL DEFAULT NOW(), - UNIQUE (email) + if _, err := c.Exec(context.Background(), `create temp table if not exists "user" ( + id uuid primary key default uuid_generate_v4(), + username text unique not null check (username <> ''), + email citext unique not null check (email ~ '^[a-zA-Z0-9.!#$%&’*+/=?^_\x60{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$'), + password citext not null check (password <> ''), + created_at timestamp not null default now() );`); err != nil { panic(err) } @@ -34,12 +34,19 @@ func init() { db = NewRepo(context.Background(), c) } -func TestSQLRepo(t *testing.T) { +func TestPSQL(t *testing.T) { t.Parallel() is, ctx := is.New(t), context.Background() - t.Run("insert two users to database", func(t *testing.T) { + t.Cleanup(func() { db.c.Close(ctx) }) + + t.Run(`select * from "user"`, func(t *testing.T) { + _, err := db.SelectMany(ctx) + is.NoErr(err) // error reading from database + }) + + t.Run(`insert two new users`, func(t *testing.T) { fizz := internal.User{ ID: suid.NewUUID(), Email: "fizz@mail.com", @@ -86,8 +93,8 @@ func TestSQLRepo(t *testing.T) { is.NoErr(err) // select user where email = "buzz@mail.com" }) - t.Run("delete user from database, return 1 user in database", func(t *testing.T) { - err := db.Remove(ctx, "fizz") + t.Run("delete by username from database, return 1 user in database", func(t *testing.T) { + err := db.Delete(ctx, "fizz") is.NoErr(err) // delete user where username == "fizz" us, err := db.SelectMany(ctx) From 3faa86f4ecb5a26f34b678167cf6091fbe202b9a Mon Sep 17 00:00:00 2001 From: adoublef Date: Sat, 8 Oct 2022 09:22:40 +0100 Subject: [PATCH 188/246] go mod tidy --- go.mod | 3 +++ go.sum | 9 +++++++++ store/sql/gen.json | 4 ++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index c3a382a2..e9eb25f5 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,8 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/goccy/go-json v0.9.11 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect github.com/kr/pretty v0.3.0 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect @@ -44,6 +46,7 @@ require ( github.com/containerd/console v1.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/go-redis/redis/v9 v9.0.0-beta.2 + github.com/jackc/pgx/v5 v5.0.1 github.com/lestrrat-go/jwx/v2 v2.0.6 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.16 // indirect diff --git a/go.sum b/go.sum index 5a468472..8044a992 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,12 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hyphengolang/prelude v0.0.7 h1:AgLkbASkg/3ioIjr0JinhReTiaeppIaX2sa7vrFwaoc= github.com/hyphengolang/prelude v0.0.7/go.mod h1:obKnO4SQhPfu3gqA9g29nbDRQLNTtmJy02i7KYZGdfM= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgx/v5 v5.0.1 h1:JZu9othr7l8so2JMDAGeDUMXqERAuZpovyfl4H50tdg= +github.com/jackc/pgx/v5 v5.0.1/go.mod h1:JBbvW3Hdw77jKl9uJrEDATUZIFM2VFPzRq4RWIhkF4o= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -116,6 +122,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= @@ -159,6 +167,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/store/sql/gen.json b/store/sql/gen.json index de1b65a3..c2c57562 100644 --- a/store/sql/gen.json +++ b/store/sql/gen.json @@ -14,8 +14,8 @@ }, { "engine": "postgresql", - "schema": "user/psql.sql", - "queries": "user/psql.sql", + "schema": "user/gen/user.sql", + "queries": "user/gen/user.sql", "gen": { "go": { "package": "user", From 67d01715a058ff1431d6c61fac6eaa62da741546 Mon Sep 17 00:00:00 2001 From: aboublef Date: Fri, 14 Oct 2022 10:12:33 +0100 Subject: [PATCH 189/246] fixed bugs --- go.mod | 3 +-- go.sum | 9 --------- internal/internal.go | 4 ++++ store/sql/user/repo.go | 4 ++++ store/sql/user/repo_test.go | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index e9eb25f5..c1692344 100644 --- a/go.mod +++ b/go.mod @@ -31,14 +31,13 @@ require ( github.com/goccy/go-json v0.9.11 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/kr/pretty v0.3.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) require ( diff --git a/go.sum b/go.sum index 8044a992..69a37e0d 100644 --- a/go.sum +++ b/go.sum @@ -53,12 +53,7 @@ github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHF github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgx/v5 v5.0.1 h1:JZu9othr7l8so2JMDAGeDUMXqERAuZpovyfl4H50tdg= github.com/jackc/pgx/v5 v5.0.1/go.mod h1:JBbvW3Hdw77jKl9uJrEDATUZIFM2VFPzRq4RWIhkF4o= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= @@ -112,7 +107,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= @@ -162,10 +156,7 @@ golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/internal.go b/internal/internal.go index 4c4be58e..c32a48f1 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -126,6 +126,8 @@ type RUserRepo interface { // can be either the "id", "email" or "username" // as these are all given unique values Select(ctx context.Context, key any) (*User, error) + + Close(ctx context.Context) error } type WUserRepo interface { @@ -135,6 +137,8 @@ type WUserRepo interface { // Performs a "hard" delete from database // Restricted to admin only Delete(ctx context.Context, key any) error + + Close(ctx context.Context) error } // Custom user type required diff --git a/store/sql/user/repo.go b/store/sql/user/repo.go index 7edd589a..fdd3de8d 100644 --- a/store/sql/user/repo.go +++ b/store/sql/user/repo.go @@ -164,6 +164,10 @@ var DefaultRepo = &repo{ logf: log.Printf, } +func (r *repo) Close(ctx context.Context) error { return nil } + +func (r *repo) Delete(ctx context.Context, key any) error { return nil } + type repo struct { mu sync.Mutex miu map[suid.UUID]*User diff --git a/store/sql/user/repo_test.go b/store/sql/user/repo_test.go index a177995f..80f143ed 100644 --- a/store/sql/user/repo_test.go +++ b/store/sql/user/repo_test.go @@ -39,7 +39,7 @@ func TestPSQL(t *testing.T) { is, ctx := is.New(t), context.Background() - t.Cleanup(func() { db.c.Close(ctx) }) + t.Cleanup(func() { db.Close(ctx) }) t.Run(`select * from "user"`, func(t *testing.T) { _, err := db.SelectMany(ctx) From cf41fd242d6937ff429e1a39c0cbe07d5a7a2825 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 14 Oct 2022 14:01:41 +0330 Subject: [PATCH 190/246] =?UTF-8?q?=F0=9F=A7=AA=20config=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ---- config/config_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 config/config_test.go diff --git a/.gitignore b/.gitignore index 7aa2f335..5d6acdca 100644 --- a/.gitignore +++ b/.gitignore @@ -22,9 +22,5 @@ bin/ # Other .DS_Store *.env -cmd/**/config certs rmx.config.json - -# NPM -node_modules diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 00000000..d1d4de5f --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,35 @@ +package config + +import ( + "reflect" + "testing" +) + +func TestConfig(t *testing.T) { + // Write config to file + i := &Config{ + ServerPort: "8000", + DBHost: "localhost", + DBPort: "3306", + DBName: "rmx", + DBUser: "rmx", + DBPassword: "password", + RedisHost: "localhost", + RedisPort: "6379", + RedisPassword: "password", + } + + if err := i.WriteToFile(); err != nil { + t.Fatal(err) + } + + // Read config from file + o, err := ScanConfigFile() + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(i, o) { + t.Fatalf("expected:\n%+v\ngot:\n%+v", i, o) + } +} From a71db26fffb470b9e134e988ad23a4a351cea607 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Sat, 15 Oct 2022 00:19:13 +0330 Subject: [PATCH 191/246] =?UTF-8?q?=E2=9D=8C=20remove=20unused=20dependenc?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/main.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index c36e20be..34d90b8d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -7,7 +7,6 @@ import ( "github.com/rog-golang-buddies/rmx/internal/commands" "github.com/urfave/cli/v2" - "github.com/urfave/cli/v2/altsrc" ) func main() { @@ -26,10 +25,7 @@ func initCLI() *cli.App { Action: func(*cli.Context) error { return nil }, - Before: altsrc.InitInputSourceWithContext( - commands.Flags, - altsrc.NewYamlSourceFromFlagFunc("load"), - ), + Flags: commands.Flags, Commands: commands.Commands, } From 7a1926f652fa0253912001eefbc1aa333b24f194 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Sat, 15 Oct 2022 00:43:07 +0330 Subject: [PATCH 192/246] =?UTF-8?q?=E2=9C=A8=20better=20dev=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + config/config.go | 29 ++- config/config_test.go | 4 +- internal/commands/commands.go | 8 +- internal/commands/run.go | 358 ++++++++++++++++------------------ 5 files changed, 202 insertions(+), 198 deletions(-) diff --git a/.gitignore b/.gitignore index 5d6acdca..a89410f5 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ bin/ *.env certs rmx.config.json +rmx-dev.config.json \ No newline at end of file diff --git a/config/config.go b/config/config.go index e90e3c0d..f02465cc 100644 --- a/config/config.go +++ b/config/config.go @@ -19,17 +19,27 @@ type Config struct { RedisPassword string `json:"redisPassword"` } -const configFileName = "rmx.config.json" +const ( + configFileName = "rmx.config.json" + devConfigFileName = "rmx-dev.config.json" +) // writes the values of the config to a file // NOTE: this will overwrite the previous generated file -func (c *Config) WriteToFile() error { +func (c *Config) WriteToFile(dev bool) error { + var fp string + if dev { + fp = devConfigFileName + } else { + fp = configFileName + } + bs, err := json.MarshalIndent(c, "", " ") if err != nil { return err } - f, err := os.OpenFile(configFileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0660) + f, err := os.OpenFile(fp, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0660) if err != nil { return err } @@ -48,9 +58,16 @@ func (c *Config) WriteToFile() error { } // checks for a config file and if one is available the value is returned -func ScanConfigFile() (*Config, error) { +func ScanConfigFile(dev bool) (*Config, error) { // check for a config file - if _, err := os.Stat(configFileName); err != nil { + var fp string + if dev { + fp = devConfigFileName + } else { + fp = configFileName + } + + if _, err := os.Stat(fp); err != nil { if errors.Is(err, os.ErrNotExist) { return nil, nil } @@ -59,7 +76,7 @@ func ScanConfigFile() (*Config, error) { } c := &Config{} - bs, err := os.ReadFile(configFileName) + bs, err := os.ReadFile(fp) if err != nil { return nil, err } diff --git a/config/config_test.go b/config/config_test.go index d1d4de5f..c502938f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -19,12 +19,12 @@ func TestConfig(t *testing.T) { RedisPassword: "password", } - if err := i.WriteToFile(); err != nil { + if err := i.WriteToFile(false); err != nil { t.Fatal(err) } // Read config from file - o, err := ScanConfigFile() + o, err := ScanConfigFile(false) if err != nil { t.Fatal(err) } diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 475cc856..ae96c318 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -32,7 +32,7 @@ var Commands = []*cli.Command{ Category: "run", Aliases: []string{"s"}, Description: "Starts the server in production mode.", - Action: runProd, + Action: run(false), // disable dev mode Flags: Flags, }, { @@ -40,9 +40,7 @@ var Commands = []*cli.Command{ Category: "run", Aliases: []string{"d"}, Description: "Starts the server in development mode", - Action: func(cCtx *cli.Context) error { - return runDev() - }, - Flags: Flags, + Action: run(true), // enable dev mode + Flags: Flags, }, } diff --git a/internal/commands/run.go b/internal/commands/run.go index e32021e6..04700ddf 100644 --- a/internal/commands/run.go +++ b/internal/commands/run.go @@ -19,237 +19,225 @@ import ( "golang.org/x/sync/errgroup" ) -func runProd(cCtx *cli.Context) error { - templates := &promptui.PromptTemplates{ - Prompt: "{{ . }} ", - Valid: "{{ . | green }} ", - Invalid: "{{ . | red }} ", - Success: "{{ . | bold }} ", - } - - // Server Port - validateNumber := func(v string) error { - if _, err := strconv.ParseUint(v, 0, 0); err != nil { - return errors.New("invalid number") +func run(dev bool) func(cCtx *cli.Context) error { + var f = func(cCtx *cli.Context) error { + templates := &promptui.PromptTemplates{ + Prompt: "{{ . }} ", + Valid: "{{ . | green }} ", + Invalid: "{{ . | red }} ", + Success: "{{ . | bold }} ", } - return nil - } + // Server Port + validateNumber := func(v string) error { + if _, err := strconv.ParseUint(v, 0, 0); err != nil { + return errors.New("invalid number") + } - validateString := func(v string) error { - if !(len(v) > 0) { - return errors.New("invalid string") + return nil } - return nil - } + validateString := func(v string) error { + if !(len(v) > 0) { + return errors.New("invalid string") + } - // check if a config file exists and use that - c, err := config.ScanConfigFile() - if err != nil { - return errors.New("failed to scan config file") - } - if c != nil { - configPrompt := promptui.Prompt{ - Label: "A config file was found. do you want to use it?", - IsConfirm: true, - Default: "y", + return nil } - validateConfirm := func(s string) error { - if len(s) == 1 && strings.Contains("YyNn", s) || - configPrompt.Default != "" && len(s) == 0 { - return nil - } - return errors.New(`invalid input (you can only use "y" or "n")`) + // check if a config file exists and use that + c, err := config.ScanConfigFile(dev) // set dev mode true/false + if err != nil { + return errors.New("failed to scan config file") } + if c != nil { + configPrompt := promptui.Prompt{ + Label: "A config file was found. do you want to use it?", + IsConfirm: true, + Default: "y", + } - configPrompt.Validate = validateConfirm + validateConfirm := func(s string) error { + if len(s) == 1 && strings.Contains("YyNn", s) || + configPrompt.Default != "" && len(s) == 0 { + return nil + } + return errors.New(`invalid input (you can only use "y" or "n")`) + } - result, err := configPrompt.Run() - if err != nil { - if strings.ToLower(result) != "n" { - return err + configPrompt.Validate = validateConfirm + + result, err := configPrompt.Run() + if err != nil { + if strings.ToLower(result) != "n" { + return err + } } - } - if strings.ToLower(result) == "y" { - return run(c) + if strings.ToLower(result) == "y" { + return serve(c) + } } - } - // Server Port - serverPortPrompt := promptui.Prompt{ - Label: "Server Port", - Validate: validateNumber, - Templates: templates, - } + // Server Port + serverPortPrompt := promptui.Prompt{ + Label: "Server Port", + Validate: validateNumber, + Templates: templates, + } - serverPort, err := serverPortPrompt.Run() - if err != nil { - return err - } + serverPort, err := serverPortPrompt.Run() + if err != nil { + return err + } - // DB Host - dbHostPrompt := promptui.Prompt{ - Label: "MySQL Database host", - Validate: validateString, - Templates: templates, - } + // DB Host + dbHostPrompt := promptui.Prompt{ + Label: "MySQL Database host", + Validate: validateString, + Templates: templates, + } - dbHost, err := dbHostPrompt.Run() - if err != nil { - return err - } + dbHost, err := dbHostPrompt.Run() + if err != nil { + return err + } - // DB Port - dbPortPrompt := promptui.Prompt{ - Label: "MySQL Database port", - Validate: validateNumber, - Templates: templates, - } + // DB Port + dbPortPrompt := promptui.Prompt{ + Label: "MySQL Database port", + Validate: validateNumber, + Templates: templates, + } - dbPort, err := dbPortPrompt.Run() - if err != nil { - return err - } + dbPort, err := dbPortPrompt.Run() + if err != nil { + return err + } - // DB Name - dbNamePrompt := promptui.Prompt{ - Label: "MySQL Database name", - Validate: validateString, - Templates: templates, - } + // DB Name + dbNamePrompt := promptui.Prompt{ + Label: "MySQL Database name", + Validate: validateString, + Templates: templates, + } - dbName, err := dbNamePrompt.Run() - if err != nil { - return err - } + dbName, err := dbNamePrompt.Run() + if err != nil { + return err + } - // DB User - dbUserPrompt := promptui.Prompt{ - Label: "MySQL Database user", - Validate: validateString, - Templates: templates, - } + // DB User + dbUserPrompt := promptui.Prompt{ + Label: "MySQL Database user", + Validate: validateString, + Templates: templates, + } - dbUser, err := dbUserPrompt.Run() - if err != nil { - return err - } + dbUser, err := dbUserPrompt.Run() + if err != nil { + return err + } - // DB Password - dbPasswordPrompt := promptui.Prompt{ - Label: "MySQL Database password", - Validate: validateString, - Templates: templates, - Mask: '*', - } + // DB Password + dbPasswordPrompt := promptui.Prompt{ + Label: "MySQL Database password", + Validate: validateString, + Templates: templates, + Mask: '*', + } - dbPassword, err := dbPasswordPrompt.Run() - if err != nil { - return err - } + dbPassword, err := dbPasswordPrompt.Run() + if err != nil { + return err + } - // Redis Host - redisHostPrompt := promptui.Prompt{ - Label: "Redis host", - Validate: validateString, - Templates: templates, - } + // Redis Host + redisHostPrompt := promptui.Prompt{ + Label: "Redis host", + Validate: validateString, + Templates: templates, + } - redisHost, err := redisHostPrompt.Run() - if err != nil { - return err - } + redisHost, err := redisHostPrompt.Run() + if err != nil { + return err + } - // Redis Port - redisPortPrompt := promptui.Prompt{ - Label: "Redis port", - Validate: validateNumber, - Templates: templates, - } + // Redis Port + redisPortPrompt := promptui.Prompt{ + Label: "Redis port", + Validate: validateNumber, + Templates: templates, + } - redisPort, err := redisPortPrompt.Run() - if err != nil { - return err - } + redisPort, err := redisPortPrompt.Run() + if err != nil { + return err + } - // Redis Password - redisPasswordPrompt := promptui.Prompt{ - Label: "Redis password", - Validate: validateString, - Templates: templates, - Mask: '*', - } + // Redis Password + redisPasswordPrompt := promptui.Prompt{ + Label: "Redis password", + Validate: validateString, + Templates: templates, + Mask: '*', + } - redisPassword, err := redisPasswordPrompt.Run() - if err != nil { - return err - } + redisPassword, err := redisPasswordPrompt.Run() + if err != nil { + return err + } - c = &config.Config{ - ServerPort: serverPort, - DBHost: dbHost, - DBPort: dbPort, - DBName: dbName, - DBUser: dbUser, - DBPassword: dbPassword, - RedisHost: redisHost, - RedisPort: redisPort, - RedisPassword: redisPassword, - } + c = &config.Config{ + ServerPort: serverPort, + DBHost: dbHost, + DBPort: dbPort, + DBName: dbName, + DBUser: dbUser, + DBPassword: dbPassword, + RedisHost: redisHost, + RedisPort: redisPort, + RedisPassword: redisPassword, + } - // prompt to save the config to a file - configPrompt := promptui.Prompt{ - Label: "Do you want to write the config to a file? (NOTE: this will rewrite the config file)", - IsConfirm: true, - Default: "n", - } + // prompt to save the config to a file + configPrompt := promptui.Prompt{ + Label: "Do you want to write the config to a file? (NOTE: this will rewrite the config file)", + IsConfirm: true, + Default: "n", + } - validateConfirm := func(s string) error { - if len(s) == 1 && strings.Contains("YyNn", s) || - configPrompt.Default != "" && len(s) == 0 { - return nil + validateConfirm := func(s string) error { + if len(s) == 1 && strings.Contains("YyNn", s) || + configPrompt.Default != "" && len(s) == 0 { + return nil + } + return errors.New(`invalid input (you can only use "y" or "n")`) } - return errors.New(`invalid input (you can only use "y" or "n")`) - } - configPrompt.Validate = validateConfirm + configPrompt.Validate = validateConfirm - result, err := configPrompt.Run() - if err != nil { - if strings.ToLower(result) != "n" { - return err + result, err := configPrompt.Run() + if err != nil { + if strings.ToLower(result) != "n" { + return err + } } - } - if strings.ToLower(result) == "y" { - if err := c.WriteToFile(); err != nil { - return err + if strings.ToLower(result) == "y" { + if err := c.WriteToFile(dev); err != nil { + return err + } } - } - - return run(c) -} -func runDev() error { - c := &config.Config{ - ServerPort: "8080", - DBHost: "localhost", - DBPort: "3306", - DBName: "rmx", - DBUser: "rmx", - DBPassword: "password", - RedisHost: "localhost", - RedisPort: "6379", - RedisPassword: "password", + return serve(c) } - return run(c) + return f } -func run(cfg *config.Config) error { +func serve(cfg *config.Config) error { sCtx, cancel := signal.NotifyContext( context.Background(), syscall.SIGHUP, From cdfa5587a30e516598de0ad2ba094fea507275ea Mon Sep 17 00:00:00 2001 From: aboublef Date: Thu, 20 Oct 2022 14:50:49 +0100 Subject: [PATCH 193/246] in process of cleaning up --- internal/internal.go | 16 ++++------------ service/{@dep => .delete}/.go | 0 2 files changed, 4 insertions(+), 12 deletions(-) rename service/{@dep => .delete}/.go (100%) diff --git a/internal/internal.go b/internal/internal.go index c32a48f1..2fff5e14 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -61,7 +61,6 @@ func (t *MsgTyp) String() string { return "NOTE_ON" case NoteOff: return "NOTE_OFF" - default: return "UNKNOWN" } @@ -119,6 +118,7 @@ type UserRepo interface { } type RUserRepo interface { + Close(ctx context.Context) error // Returns an array of users subject to any filter // conditions that are required SelectMany(ctx context.Context) ([]User, error) @@ -126,19 +126,15 @@ type RUserRepo interface { // can be either the "id", "email" or "username" // as these are all given unique values Select(ctx context.Context, key any) (*User, error) - - Close(ctx context.Context) error } type WUserRepo interface { + Close(ctx context.Context) error // Insert a new user to the database Insert(ctx context.Context, u *User) error - // Performs a "hard" delete from database // Restricted to admin only Delete(ctx context.Context, key any) error - - Close(ctx context.Context) error } // Custom user type required @@ -166,7 +162,7 @@ func (e *Email) UnmarshalJSON(b []byte) error { return e.Valid() } -// during production, this value needs to be > 40 +// During production, this value needs to be > 40 const minEntropy float64 = 50.0 // Custom password type required @@ -186,11 +182,7 @@ func (p *Password) UnmarshalJSON(b []byte) error { } func (p Password) MarshalJSON() (b []byte, err error) { - var sb strings.Builder - sb.WriteRune('"') - sb.WriteString(p.String()) - sb.WriteRune('"') - return []byte(sb.String()), nil + return []byte(`"` + p.String() + `"`), nil } func (p Password) Hash() (PasswordHash, error) { diff --git a/service/@dep/.go b/service/.delete/.go similarity index 100% rename from service/@dep/.go rename to service/.delete/.go From f74acbabdf9dccde2efb494b0209d5944b834e8c Mon Sep 17 00:00:00 2001 From: aboublef Date: Thu, 20 Oct 2022 15:42:46 +0100 Subject: [PATCH 194/246] generic helpers for sql operation added --- store/sql/author/author.sql | 34 ------- store/sql/author/author.sql.go | 89 ---------------- store/sql/author/db.go | 31 ------ store/sql/author/models.go | 15 --- store/sql/author/repo.go | 70 ------------- store/sql/author/repo_test.go | 37 ------- store/sql/user/repo.go | 180 +++++++++++++-------------------- store/sql/user/repo_test.go | 35 +++++-- 8 files changed, 96 insertions(+), 395 deletions(-) delete mode 100644 store/sql/author/author.sql delete mode 100644 store/sql/author/author.sql.go delete mode 100644 store/sql/author/db.go delete mode 100644 store/sql/author/models.go delete mode 100644 store/sql/author/repo.go delete mode 100644 store/sql/author/repo_test.go diff --git a/store/sql/author/author.sql b/store/sql/author/author.sql deleted file mode 100644 index c94958b4..00000000 --- a/store/sql/author/author.sql +++ /dev/null @@ -1,34 +0,0 @@ --- Example queries for sqlc -CREATE TEMP TABLE authors ( - id bigserial PRIMARY KEY, - name text NOT NULL, - bio text -); - --- name: GetAuthor :one -SELECT - * -FROM - authors -WHERE - id = $1 -LIMIT 1; - --- name: ListAuthors :many -SELECT - * -FROM - authors -ORDER BY - name; - --- name: CreateAuthor :one -INSERT INTO authors (name, bio) - VALUES ($1, $2) -RETURNING - *; - --- name: DeleteAuthor :exec -DELETE FROM authors -WHERE id = $1; - diff --git a/store/sql/author/author.sql.go b/store/sql/author/author.sql.go deleted file mode 100644 index 422081e2..00000000 --- a/store/sql/author/author.sql.go +++ /dev/null @@ -1,89 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.15.0 -// source: author.sql - -package author - -import ( - "context" - "database/sql" -) - -const createAuthor = `-- name: CreateAuthor :one -INSERT INTO authors (name, bio) - VALUES ($1, $2) -RETURNING - id, name, bio -` - -type CreateAuthorParams struct { - Name string - Bio sql.NullString -} - -func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) { - row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio) - var i Author - err := row.Scan(&i.ID, &i.Name, &i.Bio) - return i, err -} - -const deleteAuthor = `-- name: DeleteAuthor :exec -DELETE FROM authors -WHERE id = $1 -` - -func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error { - _, err := q.db.ExecContext(ctx, deleteAuthor, id) - return err -} - -const getAuthor = `-- name: GetAuthor :one -SELECT - id, name, bio -FROM - authors -WHERE - id = $1 -LIMIT 1 -` - -func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { - row := q.db.QueryRowContext(ctx, getAuthor, id) - var i Author - err := row.Scan(&i.ID, &i.Name, &i.Bio) - return i, err -} - -const listAuthors = `-- name: ListAuthors :many -SELECT - id, name, bio -FROM - authors -ORDER BY - name -` - -func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) { - rows, err := q.db.QueryContext(ctx, listAuthors) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Author - for rows.Next() { - var i Author - if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} diff --git a/store/sql/author/db.go b/store/sql/author/db.go deleted file mode 100644 index 309c7ff0..00000000 --- a/store/sql/author/db.go +++ /dev/null @@ -1,31 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.15.0 - -package author - -import ( - "context" - "database/sql" -) - -type DBTX interface { - ExecContext(context.Context, string, ...interface{}) (sql.Result, error) - PrepareContext(context.Context, string) (*sql.Stmt, error) - QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) - QueryRowContext(context.Context, string, ...interface{}) *sql.Row -} - -func New(db DBTX) *Queries { - return &Queries{db: db} -} - -type Queries struct { - db DBTX -} - -func (q *Queries) WithTx(tx *sql.Tx) *Queries { - return &Queries{ - db: tx, - } -} diff --git a/store/sql/author/models.go b/store/sql/author/models.go deleted file mode 100644 index 1eb5b8e7..00000000 --- a/store/sql/author/models.go +++ /dev/null @@ -1,15 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.15.0 - -package author - -import ( - "database/sql" -) - -type Author struct { - ID int64 - Name string - Bio sql.NullString -} diff --git a/store/sql/author/repo.go b/store/sql/author/repo.go deleted file mode 100644 index 79f128b7..00000000 --- a/store/sql/author/repo.go +++ /dev/null @@ -1,70 +0,0 @@ -package author - -import ( - "context" - "database/sql" -) - -const ( - psql = "postgres" - mysql = "mysql" -) - -type AuthorRepo interface { - ListAuthors(ctx context.Context) ([]Author, error) - InsertAuthor(ctx context.Context, a *InternalAuthor) (Author, error) -} - -type Repo struct { - q *Queries -} - -func NewRepo(ctx context.Context, connString string) AuthorRepo { - db, err := sql.Open(psql, connString) - if err != nil { - panic(err) - } - - // test_authors - _, err = db.ExecContext(context.Background(), ` - CREATE TEMP TABLE authors ( - id bigserial PRIMARY KEY, - name text NOT NULL, - bio text - )`) - - if err != nil { - panic(err) - } - - q := New(db) - - return &Repo{ - q, - } -} - -func (r *Repo) ListAuthors(ctx context.Context) ([]Author, error) { - return r.q.ListAuthors(ctx) -} - -func (r *Repo) InsertAuthor(ctx context.Context, a *InternalAuthor) (Author, error) { - var valid bool - if a.Bio != "" { - valid = true - } - - v := CreateAuthorParams{ - Name: a.Name, - Bio: sql.NullString{String: a.Bio, Valid: valid}, - } - - _, err := r.q.CreateAuthor(ctx, v) - return Author{}, err -} - -// this would be inside the internal package -type InternalAuthor struct { - Name string - Bio string -} diff --git a/store/sql/author/repo_test.go b/store/sql/author/repo_test.go deleted file mode 100644 index 19b74519..00000000 --- a/store/sql/author/repo_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package author - -import ( - "context" - "testing" - - "github.com/rog-golang-buddies/rmx/internal/is" - - _ "github.com/lib/pq" -) - -var r AuthorRepo - -func init() { - r = NewRepo(context.Background(), `postgres://postgres:postgrespw@localhost:49153?sslmode=disable`) -} - -func TestRepo(t *testing.T) { - t.Parallel() - is := is.New(t) - - t.Run("list all users", func(t *testing.T) { - authors, err := r.ListAuthors(context.Background()) - is.NoErr(err) // query all authors - is.Equal(len(authors), 0) // no authors in database - }) - - t.Run("insert a new author", func(t *testing.T) { - payload := &InternalAuthor{ - Name: "Brian Kernighan", - Bio: "Co-author of The C Programming Language and The Go Programming Language", - } - - _, err := r.InsertAuthor(context.Background(), payload) - is.NoErr(err) // inserting author - }) -} diff --git a/store/sql/user/repo.go b/store/sql/user/repo.go index fdd3de8d..806025ef 100644 --- a/store/sql/user/repo.go +++ b/store/sql/user/repo.go @@ -6,39 +6,42 @@ import ( "sync" "time" + psql "github.com/hyphengolang/prelude/sql/postgres" + "github.com/hyphengolang/prelude/types/email" + "github.com/hyphengolang/prelude/types/password" + "github.com/jackc/pgx/v5" + "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/suid" ) /* -CREATE TABLE users ( - - id text NOT NULL PRIMARY KEY, - username text NOT NULL, - email text NOT NULL, - password text NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - deleted_at TIMESTAMP NULL DEFAULT NULL, - UNIQUE (email) - -); +User schema +users +id uuid primary key, +text, +text unique, +text, +timestamp default::now, +timestamp nullable, +timestamp nullable, */ + type User struct { ID suid.UUID Username string - Email internal.Email - Password internal.PasswordHash + Email email.Email + Password password.PasswordHash CreatedAt time.Time - // UpdatedAt *time.Time - // DeletedAt *time.Time + UpdatedAt *time.Time + DeletedAt *time.Time } type Repo struct { ctx context.Context - // connection is using pgx until SQLC is sorted + // connection is using pgx until sqlc is sorted c *pgx.Conn } @@ -56,105 +59,66 @@ func NewRepo(ctx context.Context, conn *pgx.Conn) internal.UserRepo { return r } -func (r *Repo) Insert(ctx context.Context, iu *internal.User) error { - qry := `insert into "user" (id, email, username, password) values ($1, $2, $3, $4)` - _, err := r.c.Exec(context.Background(), qry, iu.ID, iu.Email, iu.Username, iu.Password.String()) - return err -} +const ( + qryInsert = `insert into "user" (id, email, username, password) values (@id, @email, @username, @password)` -// We need to setup the config to avoid needing to do this -func (r *Repo) SelectMany(ctx context.Context) ([]internal.User, error) { - qry := `select id, email, username, password from "user" order by id` + qrySelectMany = `select id, email, username, password from "user" order by id` - row, err := r.c.Query(context.Background(), qry) - if err != nil { - return nil, err - } - defer row.Close() + qrySelectByID = `select id, email, username, password from "user" where id = $1` + qrySelectByEmail = `select id, email, username, password from "user" where email = $1` + qrySelectByUsername = `select id, email, username, password from "user" where username = $1` - var ius []internal.User - for row.Next() { - var u User - if err := row.Scan(&u.ID, &u.Email, &u.Username, &u.Password); err != nil { - return nil, err - } + qryDeleteByID = `delete from "user" where id = $1` + qryDeleteByEmail = `delete from "user" where email = $1` + qryDeleteByUsername = `delete from "user" where username = $1` +) - iu := internal.User{ID: u.ID, Email: internal.Email(u.Email), Username: u.Username, Password: u.Password} - ius = append(ius, iu) +func (r *Repo) Insert(ctx context.Context, u *internal.User) error { + args := pgx.NamedArgs{ + "id": u.ID, + "email": u.Email, + "username": u.Username, + "password": u.Password, } - return ius, nil + return psql.Exec(r.c, qryInsert, args) } -func (r Repo) Select(ctx context.Context, key any) (*internal.User, error) { - switch key := key.(type) { +func (r *Repo) SelectMany(ctx context.Context) ([]internal.User, error) { + return psql.Query(r.c, qrySelectMany, func(r pgx.Rows, u *internal.User) error { + return r.Scan(&u.ID, &u.Email, &u.Username, &u.Password) + }) +} + +func (r *Repo) Select(ctx context.Context, key any) (*internal.User, error) { + var qry string + switch key.(type) { case suid.UUID: - return r.selectUUID(ctx, `select id, email, username, password from "user" where id = $1`, key) - case internal.Email: - return r.selectEmail(ctx, `select id, email, username, password from "user" where email = $1`, key) + qry = qrySelectByID + case email.Email: + qry = qrySelectByEmail case string: - return r.selectUsername(ctx, `select id, email, username, password from "user" where username = $1`, key) + qry = qrySelectByUsername + default: + return nil, internal.ErrInvalidType } - - return nil, internal.ErrInvalidType -} - -func (r Repo) selectUUID(ctx context.Context, qry string, uid suid.UUID) (*internal.User, error) { - row := r.c.QueryRow(ctx, qry, uid) - - var u User - err := row.Scan(&u.ID, &u.Email, &u.Username, &u.Password) - - iu := &internal.User{ID: u.ID, Email: internal.Email(u.Email), Username: u.Username, Password: u.Password} - return iu, err -} - -func (r Repo) selectEmail(ctx context.Context, qry string, email internal.Email) (*internal.User, error) { - row := r.c.QueryRow(ctx, qry, email) - - var u User - err := row.Scan(&u.ID, &u.Email, &u.Username, &u.Password) - - iu := &internal.User{ID: u.ID, Email: internal.Email(u.Email), Username: u.Username, Password: u.Password} - return iu, err -} - -func (r Repo) selectUsername(ctx context.Context, qry string, username string) (*internal.User, error) { - row := r.c.QueryRow(ctx, qry, username) - - var u User - err := row.Scan(&u.ID, &u.Email, &u.Username, &u.Password) - - iu := &internal.User{ID: u.ID, Email: internal.Email(u.Email), Username: u.Username, Password: u.Password} - return iu, err + var u internal.User + return &u, psql.QueryRow(r.c, qry, func(r pgx.Row) error { return r.Scan(&u.ID, &u.Username, &u.Email, &u.Password) }, key) } -func (r Repo) Delete(ctx context.Context, key any) error { - switch key := key.(type) { +func (r *Repo) Delete(ctx context.Context, key any) error { + var qry string + switch key.(type) { case suid.UUID: - return r.deleteUUID(ctx, `delete from "user" where id = $1`, key) - case internal.Email: - return r.deleteEmail(ctx, `delete from "user" where email = $1`, key) + qry = qryDeleteByID + case email.Email: + qry = qryDeleteByEmail case string: - return r.deleteUsername(ctx, `delete from "user" where username = $1`, key) + qry = qryDeleteByUsername + default: + return internal.ErrInvalidType } - - return internal.ErrNotImplemented -} - -func (r Repo) deleteUUID(ctx context.Context, qry string, uid suid.UUID) error { - _, err := r.c.Exec(ctx, qry, uid.String()) - return err -} - -func (r Repo) deleteEmail(ctx context.Context, qry string, email internal.Email) error { - _, err := r.c.Exec(ctx, qry, email.String()) - return err -} - -func (r Repo) deleteUsername(ctx context.Context, qry string, username string) error { - _, err := r.c.Exec(ctx, qry, username) - return err + return psql.Exec(r.c, qry, key) } var DefaultRepo = &repo{ @@ -210,7 +174,7 @@ func (r *repo) Select(ctx context.Context, key any) (*internal.User, error) { switch key := key.(type) { case suid.UUID: return r.selectUUID(key) - case internal.Email: + case email.Email: return r.selectEmail(key) case string: return r.selectUsername(key) @@ -227,8 +191,8 @@ func (r *repo) selectUUID(uid suid.UUID) (*internal.User, error) { return &internal.User{ ID: u.ID, Username: u.Username, - Email: internal.Email(u.Email), - Password: internal.PasswordHash(u.Password), + Email: email.Email(u.Email), + Password: password.PasswordHash(u.Password), }, nil } @@ -244,8 +208,8 @@ func (r *repo) selectUsername(username string) (*internal.User, error) { return &internal.User{ ID: u.ID, Username: u.Username, - Email: internal.Email(u.Email), - Password: internal.PasswordHash(u.Password), + Email: email.Email(u.Email), + Password: password.PasswordHash(u.Password), }, nil } } @@ -253,7 +217,7 @@ func (r *repo) selectUsername(username string) (*internal.User, error) { return nil, internal.ErrNotFound } -func (r *repo) selectEmail(email internal.Email) (*internal.User, error) { +func (r *repo) selectEmail(email email.Email) (*internal.User, error) { r.mu.Lock() defer r.mu.Unlock() @@ -261,8 +225,8 @@ func (r *repo) selectEmail(email internal.Email) (*internal.User, error) { return &internal.User{ ID: u.ID, Username: u.Username, - Email: internal.Email(u.Email), - Password: internal.PasswordHash(u.Password), + Email: u.Email, + Password: password.PasswordHash(u.Password), }, nil } diff --git a/store/sql/user/repo_test.go b/store/sql/user/repo_test.go index 80f143ed..40d74726 100644 --- a/store/sql/user/repo_test.go +++ b/store/sql/user/repo_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "github.com/hyphengolang/prelude/types/email" + "github.com/hyphengolang/prelude/types/password" "github.com/jackc/pgx/v5" "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/is" @@ -15,19 +17,30 @@ https://www.covermymeds.com/main/insights/articles/on-update-timestamps-mysql-vs */ var db internal.UserRepo +const migration = ` +begin; + +create extension if not exists "uuid-ossp"; +create extension if not exists "citext"; + +create temp table if not exists "user" ( + id uuid primary key default uuid_generate_v4(), + username text unique not null check (username <> ''), + email citext unique not null check (email ~ '^[a-zA-Z0-9.!#$%&’*+/=?^_\x60{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$'), + password citext not null check (password <> ''), + created_at timestamp not null default now() +); + +commit; +` + func init() { c, err := pgx.Connect(context.Background(), `postgres://postgres:postgrespw@localhost:49153/postgres?sslmode=disable`) if err != nil { panic(err) } - if _, err := c.Exec(context.Background(), `create temp table if not exists "user" ( - id uuid primary key default uuid_generate_v4(), - username text unique not null check (username <> ''), - email citext unique not null check (email ~ '^[a-zA-Z0-9.!#$%&’*+/=?^_\x60{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$'), - password citext not null check (password <> ''), - created_at timestamp not null default now() - );`); err != nil { + if _, err := c.Exec(context.Background(), migration); err != nil { panic(err) } @@ -51,7 +64,7 @@ func TestPSQL(t *testing.T) { ID: suid.NewUUID(), Email: "fizz@mail.com", Username: "fizz", - Password: internal.Password("fizz_pw_1").MustHash(), + Password: password.Password("fizz_pw_1").MustHash(), } err := db.Insert(ctx, &fizz) @@ -61,7 +74,7 @@ func TestPSQL(t *testing.T) { ID: suid.NewUUID(), Email: "buzz@mail.com", Username: "buzz", - Password: internal.Password("buzz_pw_1").MustHash(), + Password: password.Password("buzz_pw_1").MustHash(), } err = db.Insert(ctx, &buzz) @@ -77,7 +90,7 @@ func TestPSQL(t *testing.T) { ID: suid.NewUUID(), Email: "fuzz@mail.com", Username: "fizz", - Password: internal.Password("fuzz_pw_1").MustHash(), + Password: password.Password("fuzz_pw_1").MustHash(), } err := db.Insert(ctx, &fizz) @@ -89,7 +102,7 @@ func TestPSQL(t *testing.T) { is.NoErr(err) // select user where username = "fizz" is.NoErr(u.Password.Compare("fizz_pw_1")) // valid login - _, err = db.Select(ctx, internal.Email("buzz@mail.com")) + _, err = db.Select(ctx, email.Email("buzz@mail.com")) is.NoErr(err) // select user where email = "buzz@mail.com" }) From 29fac04179e26a5d234821e5a39731f5cac623d0 Mon Sep 17 00:00:00 2001 From: aboublef Date: Thu, 20 Oct 2022 15:43:05 +0100 Subject: [PATCH 195/246] move to external types package --- go.mod | 4 +-- go.sum | 3 ++ internal/auth/auth.go | 13 +++---- internal/auth/auth_test.go | 10 +++--- internal/internal.go | 74 ++++---------------------------------- internal/internal_test.go | 6 ++-- service/auth/service.go | 12 ++++--- 7 files changed, 35 insertions(+), 87 deletions(-) diff --git a/go.mod b/go.mod index c1692344..f7ca0f7c 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/go-chi/chi/v5 v5.0.7 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 - github.com/hyphengolang/prelude v0.0.7 + github.com/hyphengolang/prelude v0.1.0 github.com/lib/pq v1.10.2 github.com/lithammer/shortuuid/v4 v4.0.0 github.com/manifoldco/promptui v0.9.0 @@ -45,7 +45,7 @@ require ( github.com/containerd/console v1.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/go-redis/redis/v9 v9.0.0-beta.2 - github.com/jackc/pgx/v5 v5.0.1 + github.com/jackc/pgx/v5 v5.0.3 github.com/lestrrat-go/jwx/v2 v2.0.6 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.16 // indirect diff --git a/go.sum b/go.sum index 69a37e0d..b47f02af 100644 --- a/go.sum +++ b/go.sum @@ -47,12 +47,15 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hyphengolang/prelude v0.0.7 h1:AgLkbASkg/3ioIjr0JinhReTiaeppIaX2sa7vrFwaoc= github.com/hyphengolang/prelude v0.0.7/go.mod h1:obKnO4SQhPfu3gqA9g29nbDRQLNTtmJy02i7KYZGdfM= +github.com/hyphengolang/prelude v0.1.0 h1:Ml58zaIdC0H0pq7IxLYWl7yscX5QkJDSELz+2x6sxok= +github.com/hyphengolang/prelude v0.1.0/go.mod h1:O1Wj9q3gP0zJwsrLQKvE1hyVz9fZIwIsh+d7P8wOgOc= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgx/v5 v5.0.1 h1:JZu9othr7l8so2JMDAGeDUMXqERAuZpovyfl4H50tdg= github.com/jackc/pgx/v5 v5.0.1/go.mod h1:JBbvW3Hdw77jKl9uJrEDATUZIFM2VFPzRq4RWIhkF4o= +github.com/jackc/pgx/v5 v5.0.3/go.mod h1:JBbvW3Hdw77jKl9uJrEDATUZIFM2VFPzRq4RWIhkF4o= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= diff --git a/internal/auth/auth.go b/internal/auth/auth.go index df275a98..d585e918 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -10,6 +10,7 @@ import ( "time" "github.com/go-redis/redis/v9" + "github.com/hyphengolang/prelude/types/email" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" @@ -246,16 +247,16 @@ func ParseAuth( return } - email, ok := token.PrivateClaims()["email"].(string) + claim, ok := token.PrivateClaims()["email"].(string) if !ok { // NOTE unsure if we need to write anything more to the body w.WriteHeader(http.StatusUnauthorized) return } - // NOTE convert email from `string` type to `internal.Email` ? + // NOTE convert email from `string` type to `email.Email` ? r = r.WithContext( - context.WithValue(r.Context(), internal.EmailKey, internal.Email(email)), + context.WithValue(r.Context(), internal.EmailKey, email.Email(claim)), ) h.ServeHTTP(w, r) } @@ -277,15 +278,15 @@ func ParseAuth( return } - email, ok := token.PrivateClaims()["email"].(string) + claim, ok := token.PrivateClaims()["email"].(string) if !ok { // NOTE unsure if we need to write anything more to the body w.WriteHeader(http.StatusUnauthorized) return } - ctx := context.WithValue(r.Context(), internal.EmailKey, internal.Email(email)) - // ctx = context.WithValue(ctx, internal.EmailKey, r) + ctx := context.WithValue(r.Context(), internal.EmailKey, email.Email(claim)) + // ctx = context.WithValue(ctx, email.EmailKey, r) r = r.WithContext(context.WithValue(ctx, internal.TokenKey, token)) h.ServeHTTP(w, r) } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 2f0b7048..9390c0b1 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + "github.com/hyphengolang/prelude/types/email" + "github.com/hyphengolang/prelude/types/password" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/rog-golang-buddies/rmx/internal" @@ -26,7 +28,7 @@ func TestToken(t *testing.T) { ID: suid.NewUUID(), Username: "fizz_user", Email: "fizz@mail.com", - Password: internal.Password("492045rf-vf").MustHash(), + Password: password.Password("492045rf-vf").MustHash(), } opt := TokenOption{ @@ -58,7 +60,7 @@ func TestMiddleware(t *testing.T) { t.Run("authenticate against Authorization header", func(t *testing.T) { key := NewPairES256() - e := internal.Email("foobar@gmail.com") + e := email.Email("foobar@gmail.com") opt := TokenOption{ Issuer: "github.com/rog-golang-buddies/rmx", @@ -86,7 +88,7 @@ func TestMiddleware(t *testing.T) { t.Run("authenticate against Cookie header", func(t *testing.T) { key := NewPairES256() - e, cookieName := internal.Email("foobar@gmail.com"), `__myCookie` + e, cookieName := email.Email("foobar@gmail.com"), `__myCookie` opt := TokenOption{ Issuer: "github.com/rog-golang-buddies/rmx", @@ -121,7 +123,7 @@ func TestMiddleware(t *testing.T) { t.Run("jwk parse request", func(t *testing.T) { key := NewPairES256() - e, cookieName := internal.Email("foobar@gmail.com"), `__g` + e, cookieName := email.Email("foobar@gmail.com"), `__g` opt := TokenOption{ Issuer: "github.com/rog-golang-buddies/rmx", diff --git a/internal/internal.go b/internal/internal.go index 2fff5e14..95428873 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -3,12 +3,11 @@ package internal import ( "context" "errors" - "net/mail" "strings" + "github.com/hyphengolang/prelude/types/email" + "github.com/hyphengolang/prelude/types/password" "github.com/rog-golang-buddies/rmx/internal/suid" - gpv "github.com/wagslane/go-password-validator" - "golang.org/x/crypto/bcrypt" ) var ( @@ -139,69 +138,8 @@ type WUserRepo interface { // Custom user type required type User struct { - ID suid.UUID `json:"id"` - Username string `json:"username"` - Email Email `json:"email"` - Password PasswordHash `json:"-"` -} - -// Custom email type required -type Email string - -func (e *Email) String() string { return string(*e) } - -func (e *Email) IsValid() bool { return e.Valid() == nil } - -func (e *Email) Valid() error { - _, err := mail.ParseAddress(e.String()) - return err -} - -func (e *Email) UnmarshalJSON(b []byte) error { - *e = Email(b[1 : len(b)-1]) - return e.Valid() -} - -// During production, this value needs to be > 40 -const minEntropy float64 = 50.0 - -// Custom password type required -type Password string - -func (p Password) String() string { return string(p) } - -func (p Password) IsValid() bool { return p.Valid() == nil } - -func (p Password) Valid() error { - return gpv.Validate(p.String(), minEntropy) -} - -func (p *Password) UnmarshalJSON(b []byte) error { - *p = Password(b[1 : len(b)-1]) - return p.Valid() -} - -func (p Password) MarshalJSON() (b []byte, err error) { - return []byte(`"` + p.String() + `"`), nil -} - -func (p Password) Hash() (PasswordHash, error) { - return bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost) -} - -func (p Password) MustHash() PasswordHash { - h, err := p.Hash() - if err != nil { - panic(err) - } - - return h -} - -type PasswordHash []byte - -func (h *PasswordHash) String() string { return string(*h) } - -func (h *PasswordHash) Compare(cmp string) error { - return bcrypt.CompareHashAndPassword(*h, []byte(cmp)) + ID suid.UUID `json:"id"` + Username string `json:"username"` + Email email.Email `json:"email"` + Password password.PasswordHash `json:"-"` } diff --git a/internal/internal_test.go b/internal/internal_test.go index 826a76b7..5232809e 100644 --- a/internal/internal_test.go +++ b/internal/internal_test.go @@ -5,6 +5,8 @@ import ( "strings" "testing" + "github.com/hyphengolang/prelude/types/email" + "github.com/hyphengolang/prelude/types/password" "github.com/rog-golang-buddies/rmx/internal/is" ) @@ -16,7 +18,7 @@ func TestCustomTypes(t *testing.T) { t.Run(`using "encoding/json" package with password`, func(t *testing.T) { payload := `"this_password_is_complex"` - var p Password + var p password.Password err := json.NewDecoder(strings.NewReader(payload)).Decode(&p) is.NoErr(err) // parse password @@ -30,7 +32,7 @@ func TestCustomTypes(t *testing.T) { t.Run(`using "encoding/json" package with email`, func(t *testing.T) { payload := `"fizz@mail.com"` - var e Email + var e email.Email err := json.NewDecoder(strings.NewReader(payload)).Decode(&e) is.NoErr(err) // parse email }) diff --git a/service/auth/service.go b/service/auth/service.go index 52ac9edc..17845614 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -13,6 +13,8 @@ import ( "github.com/lestrrat-go/jwx/v2/jwt" h "github.com/hyphengolang/prelude/http" + "github.com/hyphengolang/prelude/types/email" + "github.com/hyphengolang/prelude/types/password" // github.com/rog-golang-buddies/rmx/service/internal/auth/auth "github.com/rog-golang-buddies/rmx/internal" @@ -88,7 +90,7 @@ func (s *Service) handleRefresh(key jwk.Key) http.HandlerFunc { // a 401 response. this is risky and will debate on // whether we should be more cautious j, _ := r.Context().Value(internal.TokenKey).(jwt.Token) - e, _ := r.Context().Value(internal.EmailKey).(internal.Email) + e, _ := r.Context().Value(internal.EmailKey).(email.Email) // already checked in auth but I am too tired // to come up with a cleaner solution @@ -135,7 +137,7 @@ func (s *Service) handleRefresh(key jwk.Key) http.HandlerFunc { func (s *Service) handleIdentity() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - email := r.Context().Value(internal.EmailKey).(internal.Email) + email := r.Context().Value(internal.EmailKey).(email.Email) u, err := s.r.Select(r.Context(), email) if err != nil { @@ -253,7 +255,7 @@ func (s *Service) newUser(w http.ResponseWriter, r *http.Request, u *internal.Us return } - var h internal.PasswordHash + var h password.PasswordHash h, err = dto.Password.Hash() if err != nil { return @@ -341,9 +343,9 @@ func (s *Service) Context() context.Context { } type User struct { - Email internal.Email `json:"email"` + Email email.Email `json:"email"` Username string `json:"username"` - Password internal.Password `json:"password"` + Password password.Password `json:"password"` } const ( From 9a7d1a5ce0d5fda9e1544752531686d1e3285ff2 Mon Sep 17 00:00:00 2001 From: aboublef Date: Thu, 20 Oct 2022 15:50:30 +0100 Subject: [PATCH 196/246] adding documentation inside repo/user --- store/sql/user/repo.go | 31 +++++++++++++------------------ store/store.go | 12 +++++++++++- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/store/sql/user/repo.go b/store/sql/user/repo.go index 806025ef..0cdc2774 100644 --- a/store/sql/user/repo.go +++ b/store/sql/user/repo.go @@ -16,26 +16,21 @@ import ( "github.com/rog-golang-buddies/rmx/internal/suid" ) -/* -User schema -users -id uuid primary key, -text, -text unique, -text, -timestamp default::now, -timestamp nullable, -timestamp nullable, -*/ - type User struct { - ID suid.UUID - Username string - Email email.Email - Password password.PasswordHash + // Primary key. + ID suid.UUID + // Unique. Stored as text. + Username string + // Unique. Stored as case-sensitive text. + Email email.Email + // Required. Stored as case-sensitive text. + Password password.PasswordHash + // Required. Defaults to current time. CreatedAt time.Time - UpdatedAt *time.Time - DeletedAt *time.Time + // TODO nullable, currently inactive + // UpdatedAt *time.Time + // TODO nullable, currently inactive + // DeletedAt *time.Time } type Repo struct { diff --git a/store/store.go b/store/store.go index 074c2c2d..233e5b34 100644 --- a/store/store.go +++ b/store/store.go @@ -7,6 +7,8 @@ import ( ) type Store struct { + ctx context.Context + tc internal.TokenClient ur internal.UserRepo } @@ -19,7 +21,15 @@ func (s *Store) TokenClient() internal.TokenClient { return s.tc } +// FIXME this needs to be fleshed out properly func New(ctx context.Context, connString string) *Store { - s := &Store{} + s := &Store{ctx: ctx} return s } + +func (s *Store) Context() context.Context { + if s.ctx != nil { + return s.ctx + } + return context.Background() +} From c02930b3b7ac80d66c0571cd5db7e0a212a7f396 Mon Sep 17 00:00:00 2001 From: aboublef Date: Thu, 20 Oct 2022 15:53:00 +0100 Subject: [PATCH 197/246] sorted repo/user --- store/sql/gen.json | 27 --------------------------- store/sql/user/repo.go | 1 - 2 files changed, 28 deletions(-) delete mode 100644 store/sql/gen.json diff --git a/store/sql/gen.json b/store/sql/gen.json deleted file mode 100644 index c2c57562..00000000 --- a/store/sql/gen.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "version": "2", - "sql": [ - { - "engine": "postgresql", - "schema": "author/author.sql", - "queries": "author/author.sql", - "gen": { - "go": { - "package": "author", - "out": "author" - } - } - }, - { - "engine": "postgresql", - "schema": "user/gen/user.sql", - "queries": "user/gen/user.sql", - "gen": { - "go": { - "package": "user", - "out": "user" - } - } - } - ] -} \ No newline at end of file diff --git a/store/sql/user/repo.go b/store/sql/user/repo.go index 0cdc2774..dc233c20 100644 --- a/store/sql/user/repo.go +++ b/store/sql/user/repo.go @@ -36,7 +36,6 @@ type User struct { type Repo struct { ctx context.Context - // connection is using pgx until sqlc is sorted c *pgx.Conn } From 474b6c4332184cfab8d3a33740705faa630ce740 Mon Sep 17 00:00:00 2001 From: aboublef Date: Thu, 20 Oct 2022 16:44:13 +0100 Subject: [PATCH 198/246] reverting auth to version that passed tests --- internal/auth/auth.go | 109 ++++++++++++++------ internal/auth/auth_test.go | 20 ++-- service/auth/service.go | 198 ++++++++++++++++++++----------------- 3 files changed, 195 insertions(+), 132 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index d585e918..25f2e4ab 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -16,7 +16,6 @@ import ( "github.com/lestrrat-go/jwx/v2/jwt" "github.com/pkg/errors" "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/fp" ) type Client struct { @@ -117,7 +116,7 @@ type Pair [2]jwk.Key func (p *Pair) Private() jwk.Key { return p[0] } func (p *Pair) Public() jwk.Key { return p[1] } -func NewPairES256() Pair { +func ES256() Pair { rawPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { panic(err) @@ -141,7 +140,7 @@ func NewPairES256() Pair { return Pair{key, pub} } -func NewPairRS256() Pair { +func RS256() Pair { rawPrv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { panic(err) @@ -160,44 +159,95 @@ func NewPairRS256() Pair { return Pair{jwkPrv, jwkPub} } -func SignToken(key jwk.Key, opt *TokenOption) ([]byte, error) { - var t time.Time - if opt.IssuedAt.IsZero() { - t = time.Now().UTC() +func Sign(key jwk.Key, o *TokenOption) ([]byte, error) { + var sep jwt.SignEncryptParseOption + switch key := key.(type) { + case jwk.RSAPrivateKey: + sep = jwt.WithKey(jwa.RS256, key) + case jwk.ECDSAPrivateKey: + sep = jwt.WithKey(jwa.ES256, key) + default: + return nil, errors.New(`unsupported encryption`) + } + + var iat time.Time + if o.IssuedAt.IsZero() { + iat = time.Now().UTC() } else { - t = opt.IssuedAt + iat = o.IssuedAt } - token, err := jwt.NewBuilder(). - Issuer(opt.Issuer). - Audience(opt.Audience). - Subject(opt.Subject). - IssuedAt(t). - Expiration(t.Add(opt.Expiration)). + tk, err := jwt.NewBuilder(). + Issuer(o.Issuer). + Audience(o.Audience). + Subject(o.Subject). + IssuedAt(iat). + Expiration(iat.Add(o.Expiration)). Build() + if err != nil { return nil, ErrSignTokens } - for _, c := range opt.Claims { - if !c.HasValue() { - return nil, fp.ErrTuple + for k, v := range o.Claims { + if err := tk.Set(k, v); err != nil { + return nil, err } + } - err := token.Set(c[0], c[1]) - if err != nil { - return nil, ErrSignTokens - } + return jwt.Sign(tk, sep) +} + +func ParseCookie(r *http.Request, key jwk.Key, cookieName string) (jwt.Token, error) { + c, err := r.Cookie(cookieName) + if err != nil { + return nil, err } - var algo jwa.SignatureAlgorithm - if opt.Algo == "" { - algo = jwa.RS256 - } else { - algo = opt.Algo + var sep jwt.SignEncryptParseOption + switch key := key.(type) { + case jwk.RSAPublicKey: + sep = jwt.WithKey(jwa.RS256, key) + case jwk.ECDSAPublicKey: + sep = jwt.WithKey(jwa.ES256, key) + default: + return nil, errors.New(`unsupported encryption`) } - return jwt.Sign(token, jwt.WithKey(algo, key)) + return jwt.Parse([]byte(c.Value), sep) +} + +/* +ParseRequest searches a http.Request object for a JWT token. + +Specifying WithHeaderKey() will tell it to search under a specific +header key. Specifying WithFormKey() will tell it to search under +a specific form field. + +By default, "Authorization" header will be searched. + +If WithHeaderKey() is used, you must explicitly re-enable searching for "Authorization" header. + + # searches for "Authorization" + jwt.ParseRequest(req) + + # searches for "x-my-token" ONLY. + jwt.ParseRequest(req, jwt.WithHeaderKey("x-my-token")) + + # searches for "Authorization" AND "x-my-token" + jwt.ParseRequest(req, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey("x-my-token")) +*/ +func ParseRequest(r *http.Request, key jwk.Key) (jwt.Token, error) { + var sep jwt.SignEncryptParseOption + switch key := key.(type) { + case jwk.RSAPublicKey: + sep = jwt.WithKey(jwa.RS256, key) + case jwk.ECDSAPublicKey: + sep = jwt.WithKey(jwa.ES256, key) + default: + return nil, errors.New(`unsupported encryption`) + } + return jwt.ParseRequest(r, sep) } func ParseRefreshTokenClaims(token string) (jwt.Token, error) { return jwt.Parse([]byte(token)) } @@ -214,13 +264,12 @@ func ParseRefreshTokenWithValidate(key *jwk.Key, token string) (jwt.Token, error } type TokenOption struct { + IssuedAt time.Time Issuer string Audience []string Subject string - Claims []fp.Tuple - IssuedAt time.Time Expiration time.Duration - Algo jwa.SignatureAlgorithm + Claims map[string]any } type authCtxKey string diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 9390c0b1..0f79babf 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -22,7 +22,7 @@ func TestToken(t *testing.T) { is := is.New(t) t.Run(`generate a token and sign`, func(t *testing.T) { - key := NewPairES256() + key := ES256() u := internal.User{ ID: suid.NewUUID(), @@ -39,16 +39,16 @@ func TestToken(t *testing.T) { Algo: jwa.ES256, } - _, err := SignToken(key.Private(), &opt) + _, err := Sign(key.Private(), &opt) is.NoErr(err) // sign id token opt.Subject = u.ID.String() opt.Expiration = AccessTokenExpiry - _, err = SignToken(key.Private(), &opt) + _, err = Sign(key.Private(), &opt) is.NoErr(err) // access token opt.Expiration = RefreshTokenExpiry - _, err = SignToken(key.Private(), &opt) + _, err = Sign(key.Private(), &opt) is.NoErr(err) // refresh token }) } @@ -58,7 +58,7 @@ func TestMiddleware(t *testing.T) { is := is.New(t) t.Run("authenticate against Authorization header", func(t *testing.T) { - key := NewPairES256() + key := ES256() e := email.Email("foobar@gmail.com") @@ -71,7 +71,7 @@ func TestMiddleware(t *testing.T) { } // ats - ats, err := SignToken(key.Private(), &opt) + ats, err := Sign(key.Private(), &opt) is.NoErr(err) // signing access token h := ParseAuth(opt.Algo, key.Public())(http.NotFoundHandler()) @@ -86,7 +86,7 @@ func TestMiddleware(t *testing.T) { }) t.Run("authenticate against Cookie header", func(t *testing.T) { - key := NewPairES256() + key := ES256() e, cookieName := email.Email("foobar@gmail.com"), `__myCookie` @@ -99,7 +99,7 @@ func TestMiddleware(t *testing.T) { } // rts - rts, err := SignToken(key.Private(), &opt) + rts, err := Sign(key.Private(), &opt) is.NoErr(err) // signing refresh token h := ParseAuth(opt.Algo, key.Public(), cookieName)(http.NotFoundHandler()) @@ -121,7 +121,7 @@ func TestMiddleware(t *testing.T) { }) t.Run("jwk parse request", func(t *testing.T) { - key := NewPairES256() + key := ES256() e, cookieName := email.Email("foobar@gmail.com"), `__g` @@ -134,7 +134,7 @@ func TestMiddleware(t *testing.T) { } // rts - rts, err := SignToken(key.Private(), &opt) + rts, err := Sign(key.Private(), &opt) is.NoErr(err) // signing refresh token c := &http.Cookie{ diff --git a/service/auth/service.go b/service/auth/service.go index 17845614..972552aa 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -8,9 +8,7 @@ import ( "time" "github.com/go-chi/chi/v5" - "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" h "github.com/hyphengolang/prelude/http" "github.com/hyphengolang/prelude/types/email" @@ -19,7 +17,6 @@ import ( // github.com/rog-golang-buddies/rmx/service/internal/auth/auth "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/auth" - "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/suid" // big no-no ) @@ -59,58 +56,95 @@ Refresh token [?] GET /auth/refresh */ +type Service struct { + ctx context.Context + + m chi.Router + + r internal.UserRepo + tc internal.TokenClient + + log func(...any) + logf func(string, ...any) + + decode func(http.ResponseWriter, *http.Request, any) error + respond func(http.ResponseWriter, *http.Request, any, int) + created func(http.ResponseWriter, *http.Request, string) + setCookie func(http.ResponseWriter, *http.Cookie) +} + func (s *Service) routes() { - key := auth.NewPairES256() + key := auth.ES256() s.m.Route("/api/v1/auth", func(r chi.Router) { r.Post("/sign-in", s.handleSignIn(key.Private())) r.Delete("/sign-out", s.handleSignOut()) r.Post("/sign-up", s.handleSignUp()) - auth := r.With( - auth.ParseAuth(jwa.ES256, key.Public(), cookieName), - ) // passing cookie is required - auth.Get("/refresh", s.handleRefresh(key.Private())) + r.Get("/refresh", s.handleRefresh(key.Public(), key.Private())) }) s.m.Route("/api/v1/account", func(r chi.Router) { - auth := r.With(auth.ParseAuth(jwa.ES256, key.Public())) - auth.Get("/me", s.handleIdentity()) + // auth := r.With(auth.ParseAuth(jwa.ES256, key.Public())) + r.Get("/me", s.handleIdentity(key.Public())) }) } -func (s *Service) handleRefresh(key jwk.Key) http.HandlerFunc { +// FIXME this endpoint is broken due to the redis client +// We need to try fix this ASAP +func (s *Service) handleRefresh(public, private jwk.Key) http.HandlerFunc { type token struct { AccessToken string `json:"accessToken"` } return func(w http.ResponseWriter, r *http.Request) { - - // this have to exist else it would have lead to - // a 401 response. this is risky and will debate on - // whether we should be more cautious - j, _ := r.Context().Value(internal.TokenKey).(jwt.Token) - e, _ := r.Context().Value(internal.EmailKey).(email.Email) - - // already checked in auth but I am too tired - // to come up with a cleaner solution - k, _ := r.Cookie(cookieName) - - err := s.tc.ValidateRefreshToken(r.Context(), k.Value) + // NOTE temp switch away from auth middleware + jtk, err := auth.ParseCookie(r, public, cookieName) if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) + s.respond(w, r, err, http.StatusUnauthorized) return } - // token validated, now it should be set inside blacklist - // this prevents token reuse - err = s.tc.BlackListRefreshToken(r.Context(), k.Value) + claim, ok := jtk.PrivateClaims()["email"].(string) + if !ok { + s.respondText(w, r, http.StatusInternalServerError) + return + } + + u, err := s.r.Select(r.Context(), email.Email(claim)) if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) + s.respond(w, r, err, http.StatusForbidden) + return } - cid := j.Subject() - _, ats, rts, err := s.signedTokens(key, e.String(), suid.SUID(cid)) + // FIXME commented out as not complete + // // already checked in auth but I am too tired + // // to come up with a cleaner solution + // k, _ := r.Cookie(cookieName) + + // err := s.tc.ValidateRefreshToken(r.Context(), k.Value) + // if err != nil { + // s.respond(w, r, err, http.StatusInternalServerError) + // return + // } + + // // token validated, now it should be set inside blacklist + // // this prevents token reuse + // err = s.tc.BlackListRefreshToken(r.Context(), k.Value) + // if err != nil { + // s.respond(w, r, err, http.StatusInternalServerError) + // } + + // cid := j.Subject() + // _, ats, rts, err := s.signedTokens(private, claim.String(), suid.SUID(cid)) + // if err != nil { + // s.respond(w, r, err, http.StatusInternalServerError) + // return + // } + + u.ID = suid.MustParse(jtk.Subject()) + + _, ats, rts, err := s.signedTokens(private, u) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) return @@ -135,11 +169,22 @@ func (s *Service) handleRefresh(key jwk.Key) http.HandlerFunc { } } -func (s *Service) handleIdentity() http.HandlerFunc { +func (s *Service) handleIdentity(public jwk.Key) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - email := r.Context().Value(internal.EmailKey).(email.Email) + // NOTE temp switch away from auth middleware + tk, err := auth.ParseRequest(r, public) + if err != nil { + s.respond(w, r, err, http.StatusUnauthorized) + return + } + + claim, ok := tk.PrivateClaims()["email"].(string) + if !ok { + s.respondText(w, r, http.StatusInternalServerError) + return + } - u, err := s.r.Select(r.Context(), email) + u, err := s.r.Select(r.Context(), email.Email(claim)) if err != nil { s.respond(w, r, err, http.StatusNotFound) return @@ -173,7 +218,11 @@ func (s *Service) handleSignIn(privateKey jwk.Key) http.HandlerFunc { return } - its, ats, rts, err := s.signedTokens(privateKey, u.Email.String(), suid.NewSUID()) + // need to replace u.UUID with a client based ID + // this will mean different cookies for multi-device usage + u.ID = suid.NewUUID() + + its, ats, rts, err := s.signedTokens(privateKey, u) if err != nil { s.respond(w, r, err, http.StatusInternalServerError) return @@ -232,21 +281,8 @@ func (s *Service) handleSignUp() http.HandlerFunc { } } -type Service struct { - ctx context.Context - - m chi.Router - - r internal.UserRepo - tc internal.TokenClient - - log func(...any) - logf func(string, ...any) - - decode func(http.ResponseWriter, *http.Request, any) error - respond func(http.ResponseWriter, *http.Request, any, int) - created func(http.ResponseWriter, *http.Request, string) - setCookie func(http.ResponseWriter, *http.Cookie) +func (s *Service) respondText(w http.ResponseWriter, r *http.Request, status int) { + s.respond(w, r, http.StatusText(status), status) } func (s *Service) newUser(w http.ResponseWriter, r *http.Request, u *internal.User) (err error) { @@ -276,61 +312,39 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, } // TODO there is two cid's being used here, need clarification -func (s *Service) signedTokens( - key jwk.Key, - email string, - cid suid.SUID, -) (its, ats, rts []byte, err error) { - opt := auth.TokenOption{ - Issuer: "github.com/rog-golang-buddies/rmx", - Subject: cid.String(), // new client ID for tracking user connections - Expiration: time.Hour * 10, - Claims: []fp.Tuple{{"email", email}}, - Algo: jwa.ES256, +func (s *Service) signedTokens(private jwk.Key, u *internal.User) (its, ats, rts []byte, err error) { + o := auth.TokenOption{ + Issuer: "github.com/rog-golang-buddies/rmx", + Subject: u.ID.ShortUUID().String(), // new client ID for tracking user connections + // Audience: []string{}, + Claims: map[string]any{"email": u.Email}, } - if its, err = auth.SignToken(key, &opt); err != nil { - return nil, nil, nil, err + // its + o.Expiration = time.Hour * 10 + if its, err = auth.Sign(private, &o); err != nil { + return } - opt.Expiration = time.Minute * 5 - if ats, err = auth.SignToken(key, &opt); err != nil { - return nil, nil, nil, err + // ats + o.Expiration = time.Minute * 5 + if ats, err = auth.Sign(private, &o); err != nil { + return } - opt.Expiration = time.Hour * 24 * 7 - if rts, err = auth.SignToken(key, &opt); err != nil { - return nil, nil, nil, err + // rts + o.Expiration = time.Hour * 24 * 7 + if rts, err = auth.Sign(private, &o); err != nil { + return } - return its, ats, rts, nil + return } func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func NewService( - ctx context.Context, - m chi.Router, - r internal.UserRepo, - tc internal.TokenClient, -) *Service { - - s := &Service{ - ctx, - - m, - r, - tc, - - log.Println, - log.Printf, - - h.Decode, - h.Respond, - h.Created, - http.SetCookie, - } - +func NewService(ctx context.Context, m chi.Router, r internal.UserRepo, tc internal.TokenClient) *Service { + s := &Service{ctx, m, r, tc, log.Println, log.Printf, h.Decode, h.Respond, h.Created, http.SetCookie} s.routes() return s } From c819f50ddb81cd2f813855054a6d3c7d74d7dc22 Mon Sep 17 00:00:00 2001 From: aboublef Date: Thu, 20 Oct 2022 19:23:17 +0100 Subject: [PATCH 199/246] auth api changed, removed the Pair type --- internal/auth/auth.go | 29 ++++++++++------------------- service/auth/service.go | 11 +++++------ 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 25f2e4ab..27b40adf 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -116,47 +116,38 @@ type Pair [2]jwk.Key func (p *Pair) Private() jwk.Key { return p[0] } func (p *Pair) Public() jwk.Key { return p[1] } -func ES256() Pair { - rawPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) +func ES256() (public, private jwk.Key) { + raw, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { panic(err) } - key, err := jwk.FromRaw(rawPriv) - if err != nil { + if private, err = jwk.FromRaw(raw); err != nil { panic(err) } - _, ok := key.(jwk.ECDSAPrivateKey) - if !ok { - panic(ErrGenerateKey) - } - - pub, err := key.PublicKey() - if err != nil { + if public, err = private.PublicKey(); err != nil { panic(err) } - return Pair{key, pub} + return } -func RS256() Pair { - rawPrv, err := rsa.GenerateKey(rand.Reader, 2048) +func RS256() (public, private jwk.Key) { + raw, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { panic(err) } - jwkPrv, err := jwk.FromRaw(rawPrv) - if err != nil { + if private, err = jwk.FromRaw(raw); err != nil { panic(err) } - jwkPub, err := jwkPrv.PublicKey() - if err != nil { + if public, err = private.PublicKey(); err != nil { panic(err) } - return Pair{jwkPrv, jwkPub} + return } func Sign(key jwk.Key, o *TokenOption) ([]byte, error) { diff --git a/service/auth/service.go b/service/auth/service.go index 972552aa..3f2216e4 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -74,19 +74,18 @@ type Service struct { } func (s *Service) routes() { - key := auth.ES256() + public, private := auth.ES256() s.m.Route("/api/v1/auth", func(r chi.Router) { - r.Post("/sign-in", s.handleSignIn(key.Private())) + r.Post("/sign-in", s.handleSignIn(private)) r.Delete("/sign-out", s.handleSignOut()) r.Post("/sign-up", s.handleSignUp()) - r.Get("/refresh", s.handleRefresh(key.Public(), key.Private())) + r.Get("/refresh", s.handleRefresh(public, private)) }) s.m.Route("/api/v1/account", func(r chi.Router) { - // auth := r.With(auth.ParseAuth(jwa.ES256, key.Public())) - r.Get("/me", s.handleIdentity(key.Public())) + r.Get("/me", s.handleIdentity(public)) }) } @@ -142,7 +141,7 @@ func (s *Service) handleRefresh(public, private jwk.Key) http.HandlerFunc { // return // } - u.ID = suid.MustParse(jtk.Subject()) + u.ID, _ = suid.ParseString(jtk.Subject()) _, ats, rts, err := s.signedTokens(private, u) if err != nil { From 6cae235f2e0f43c3224fe6da28cd34c1816c1b44 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Fri, 21 Oct 2022 00:28:54 +0330 Subject: [PATCH 200/246] =?UTF-8?q?=F0=9F=AA=9B=20=20fix=20auth=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 5 ++--- go.sum | 7 +------ internal/auth/auth.go | 16 +++++++++------- internal/auth/auth_test.go | 39 +++++++++++++++++--------------------- service/auth/service.go | 8 ++++---- 5 files changed, 33 insertions(+), 42 deletions(-) diff --git a/go.mod b/go.mod index f7ca0f7c..32ca5303 100644 --- a/go.mod +++ b/go.mod @@ -11,13 +11,11 @@ require ( github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 github.com/hyphengolang/prelude v0.1.0 - github.com/lib/pq v1.10.2 github.com/lithammer/shortuuid/v4 v4.0.0 github.com/manifoldco/promptui v0.9.0 github.com/pkg/errors v0.9.1 github.com/rs/cors v1.8.2 github.com/urfave/cli/v2 v2.16.3 - golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 golang.org/x/exp v0.0.0-20220921164117-439092de6870 golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 golang.org/x/term v0.0.0-20220919170432-7a66f970e087 @@ -38,6 +36,7 @@ require ( github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect + golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect ) require ( @@ -57,7 +56,7 @@ require ( github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/wagslane/go-password-validator v0.3.0 + github.com/wagslane/go-password-validator v0.3.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect golang.org/x/text v0.3.7 // indirect diff --git a/go.sum b/go.sum index b47f02af..d4e1ef50 100644 --- a/go.sum +++ b/go.sum @@ -45,16 +45,13 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hyphengolang/prelude v0.0.7 h1:AgLkbASkg/3ioIjr0JinhReTiaeppIaX2sa7vrFwaoc= -github.com/hyphengolang/prelude v0.0.7/go.mod h1:obKnO4SQhPfu3gqA9g29nbDRQLNTtmJy02i7KYZGdfM= github.com/hyphengolang/prelude v0.1.0 h1:Ml58zaIdC0H0pq7IxLYWl7yscX5QkJDSELz+2x6sxok= github.com/hyphengolang/prelude v0.1.0/go.mod h1:O1Wj9q3gP0zJwsrLQKvE1hyVz9fZIwIsh+d7P8wOgOc= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgx/v5 v5.0.1 h1:JZu9othr7l8so2JMDAGeDUMXqERAuZpovyfl4H50tdg= -github.com/jackc/pgx/v5 v5.0.1/go.mod h1:JBbvW3Hdw77jKl9uJrEDATUZIFM2VFPzRq4RWIhkF4o= +github.com/jackc/pgx/v5 v5.0.3 h1:4flM5ecR/555F0EcnjdaZa6MhBU+nr0QbZIo5vaKjuM= github.com/jackc/pgx/v5 v5.0.3/go.mod h1:JBbvW3Hdw77jKl9uJrEDATUZIFM2VFPzRq4RWIhkF4o= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -72,8 +69,6 @@ github.com/lestrrat-go/jwx/v2 v2.0.6 h1:RlyYNLV892Ed7+FTfj1ROoF6x7WxL965PGTHso/6 github.com/lestrrat-go/jwx/v2 v2.0.6/go.mod h1:aVrGuwEr3cp2Prw6TtQvr8sQxe+84gruID5C9TxT64Q= github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= -github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c= github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 27b40adf..dc9e9cac 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -111,26 +111,28 @@ func (c *Client) ValidateClientID(ctx context.Context, cid string) error { } // Easier to pass an array that two variables with context -type Pair [2]jwk.Key +type KeyPair [2]jwk.Key -func (p *Pair) Private() jwk.Key { return p[0] } -func (p *Pair) Public() jwk.Key { return p[1] } +func (p *KeyPair) Private() jwk.Key { return p[0] } +func (p *KeyPair) Public() jwk.Key { return p[1] } -func ES256() (public, private jwk.Key) { +func ES256() *KeyPair { raw, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { panic(err) } - if private, err = jwk.FromRaw(raw); err != nil { + private, err := jwk.FromRaw(raw) + if err != nil { panic(err) } - if public, err = private.PublicKey(); err != nil { + public, err := private.PublicKey() + if err != nil { panic(err) } - return + return &KeyPair{private, public} } func RS256() (public, private jwk.Key) { diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 0f79babf..3866ab8c 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -12,7 +12,6 @@ import ( "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/is" "github.com/rog-golang-buddies/rmx/internal/suid" ) @@ -22,7 +21,7 @@ func TestToken(t *testing.T) { is := is.New(t) t.Run(`generate a token and sign`, func(t *testing.T) { - key := ES256() + kp := ES256() u := internal.User{ ID: suid.NewUUID(), @@ -35,20 +34,19 @@ func TestToken(t *testing.T) { Issuer: "github.com/rog-golang-buddies/rmx", Subject: suid.NewUUID().String(), Expiration: time.Hour * 10, - Claims: []fp.Tuple{{"email", u.Email.String()}}, - Algo: jwa.ES256, + Claims: map[string]any{"email": u.Email.String()}, } - _, err := Sign(key.Private(), &opt) + _, err := Sign(kp.Private(), &opt) is.NoErr(err) // sign id token opt.Subject = u.ID.String() opt.Expiration = AccessTokenExpiry - _, err = Sign(key.Private(), &opt) + _, err = Sign(kp.Private(), &opt) is.NoErr(err) // access token opt.Expiration = RefreshTokenExpiry - _, err = Sign(key.Private(), &opt) + _, err = Sign(kp.Private(), &opt) is.NoErr(err) // refresh token }) } @@ -58,7 +56,7 @@ func TestMiddleware(t *testing.T) { is := is.New(t) t.Run("authenticate against Authorization header", func(t *testing.T) { - key := ES256() + kp := ES256() e := email.Email("foobar@gmail.com") @@ -66,15 +64,14 @@ func TestMiddleware(t *testing.T) { Issuer: "github.com/rog-golang-buddies/rmx", Subject: suid.NewUUID().String(), Expiration: time.Hour * 10, - Claims: []fp.Tuple{{"email", e.String()}}, - Algo: jwa.ES256, + Claims: map[string]any{"email": e.String()}, } // ats - ats, err := Sign(key.Private(), &opt) + ats, err := Sign(kp.Private(), &opt) is.NoErr(err) // signing access token - h := ParseAuth(opt.Algo, key.Public())(http.NotFoundHandler()) + h := ParseAuth(jwa.ES256, kp.Public())(http.NotFoundHandler()) req, _ := http.NewRequest(http.MethodGet, "/", nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(ats))) @@ -86,7 +83,7 @@ func TestMiddleware(t *testing.T) { }) t.Run("authenticate against Cookie header", func(t *testing.T) { - key := ES256() + kp := ES256() e, cookieName := email.Email("foobar@gmail.com"), `__myCookie` @@ -94,15 +91,14 @@ func TestMiddleware(t *testing.T) { Issuer: "github.com/rog-golang-buddies/rmx", Subject: suid.NewUUID().String(), Expiration: time.Hour * 10, - Claims: []fp.Tuple{{"email", e.String()}}, - Algo: jwa.ES256, + Claims: map[string]any{"email": e.String()}, } // rts - rts, err := Sign(key.Private(), &opt) + rts, err := Sign(kp.Private(), &opt) is.NoErr(err) // signing refresh token - h := ParseAuth(opt.Algo, key.Public(), cookieName)(http.NotFoundHandler()) + h := ParseAuth(jwa.ES256, kp.Public(), cookieName)(http.NotFoundHandler()) req, _ := http.NewRequest(http.MethodGet, "/", nil) req.AddCookie(&http.Cookie{ @@ -121,7 +117,7 @@ func TestMiddleware(t *testing.T) { }) t.Run("jwk parse request", func(t *testing.T) { - key := ES256() + kp := ES256() e, cookieName := email.Email("foobar@gmail.com"), `__g` @@ -129,12 +125,11 @@ func TestMiddleware(t *testing.T) { Issuer: "github.com/rog-golang-buddies/rmx", Subject: suid.NewUUID().String(), Expiration: time.Hour * 10, - Claims: []fp.Tuple{{"email", e.String()}}, - Algo: jwa.ES256, + Claims: map[string]any{"email": e.String()}, } // rts - rts, err := Sign(key.Private(), &opt) + rts, err := Sign(kp.Private(), &opt) is.NoErr(err) // signing refresh token c := &http.Cookie{ @@ -150,7 +145,7 @@ func TestMiddleware(t *testing.T) { _, err = jwt.Parse( []byte(c.Value), - jwt.WithKey(opt.Algo, key.Public()), + jwt.WithKey(jwa.ES256, kp.Public()), jwt.WithValidate(true), ) is.NoErr(err) // parsing jwk page not found diff --git a/service/auth/service.go b/service/auth/service.go index 3f2216e4..5a26b4a5 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -74,18 +74,18 @@ type Service struct { } func (s *Service) routes() { - public, private := auth.ES256() + kp := auth.ES256() s.m.Route("/api/v1/auth", func(r chi.Router) { - r.Post("/sign-in", s.handleSignIn(private)) + r.Post("/sign-in", s.handleSignIn(kp.Private())) r.Delete("/sign-out", s.handleSignOut()) r.Post("/sign-up", s.handleSignUp()) - r.Get("/refresh", s.handleRefresh(public, private)) + r.Get("/refresh", s.handleRefresh(kp.Public(), kp.Private())) }) s.m.Route("/api/v1/account", func(r chi.Router) { - r.Get("/me", s.handleIdentity(public)) + r.Get("/me", s.handleIdentity(kp.Public())) }) } From b62e5f6d77f1a49985bca2323a4d7a6d756f6140 Mon Sep 17 00:00:00 2001 From: aboublef Date: Thu, 20 Oct 2022 22:42:08 +0100 Subject: [PATCH 201/246] auth_test fixed --- internal/auth/auth.go | 75 ----------------- internal/auth/auth_test.go | 104 ++++------------------- internal/internal_test.go | 2 +- internal/is/io.go | 112 ------------------------- internal/is/is.go | 157 ----------------------------------- service/auth/service_test.go | 2 +- service/jam/service.go | 6 +- store/sql/user/repo_test.go | 2 +- 8 files changed, 21 insertions(+), 439 deletions(-) delete mode 100644 internal/is/io.go delete mode 100644 internal/is/is.go diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 27b40adf..aeca6e9f 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -10,12 +10,10 @@ import ( "time" "github.com/go-redis/redis/v9" - "github.com/hyphengolang/prelude/types/email" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/pkg/errors" - "github.com/rog-golang-buddies/rmx/internal" ) type Client struct { @@ -110,12 +108,6 @@ func (c *Client) ValidateClientID(ctx context.Context, cid string) error { return ErrRTValidate } -// Easier to pass an array that two variables with context -type Pair [2]jwk.Key - -func (p *Pair) Private() jwk.Key { return p[0] } -func (p *Pair) Public() jwk.Key { return p[1] } - func ES256() (public, private jwk.Key) { raw, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { @@ -271,70 +263,3 @@ const ( AccessTokenExpiry = time.Minute * 5 EmailKey = authCtxKey("rmx-email") ) - -/* - */ -func ParseAuth( - algo jwa.SignatureAlgorithm, - key jwk.Key, - cookieName ...string, -) func(http.Handler) http.Handler { - return func(h http.Handler) http.Handler { - var at http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { - token, err := jwt.ParseRequest(r, jwt.WithKey(algo, key)) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - claim, ok := token.PrivateClaims()["email"].(string) - if !ok { - // NOTE unsure if we need to write anything more to the body - w.WriteHeader(http.StatusUnauthorized) - return - } - - // NOTE convert email from `string` type to `email.Email` ? - r = r.WithContext( - context.WithValue(r.Context(), internal.EmailKey, email.Email(claim)), - ) - h.ServeHTTP(w, r) - } - - var rt http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) { - rc, err := r.Cookie(cookieName[0]) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - token, err := jwt.Parse( - []byte(rc.Value), - jwt.WithKey(algo, key), - jwt.WithValidate(true), - ) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - return - } - - claim, ok := token.PrivateClaims()["email"].(string) - if !ok { - // NOTE unsure if we need to write anything more to the body - w.WriteHeader(http.StatusUnauthorized) - return - } - - ctx := context.WithValue(r.Context(), internal.EmailKey, email.Email(claim)) - // ctx = context.WithValue(ctx, email.EmailKey, r) - r = r.WithContext(context.WithValue(ctx, internal.TokenKey, token)) - h.ServeHTTP(w, r) - } - - if len(cookieName) != 0 { - return http.HandlerFunc(rt) - } - - return http.HandlerFunc(at) - } -} diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 0f79babf..bc616d08 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -1,19 +1,16 @@ package auth import ( - "fmt" "net/http" - "net/http/httptest" "testing" "time" + "github.com/hyphengolang/prelude/testing/is" "github.com/hyphengolang/prelude/types/email" "github.com/hyphengolang/prelude/types/password" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/fp" - "github.com/rog-golang-buddies/rmx/internal/is" "github.com/rog-golang-buddies/rmx/internal/suid" ) @@ -22,7 +19,7 @@ func TestToken(t *testing.T) { is := is.New(t) t.Run(`generate a token and sign`, func(t *testing.T) { - key := ES256() + _, private := ES256() u := internal.User{ ID: suid.NewUUID(), @@ -31,24 +28,24 @@ func TestToken(t *testing.T) { Password: password.Password("492045rf-vf").MustHash(), } - opt := TokenOption{ + o := TokenOption{ Issuer: "github.com/rog-golang-buddies/rmx", Subject: suid.NewUUID().String(), Expiration: time.Hour * 10, - Claims: []fp.Tuple{{"email", u.Email.String()}}, - Algo: jwa.ES256, + Claims: map[string]any{"email": u.Email}, } - _, err := Sign(key.Private(), &opt) + _, err := Sign(private, &o) is.NoErr(err) // sign id token - opt.Subject = u.ID.String() - opt.Expiration = AccessTokenExpiry - _, err = Sign(key.Private(), &opt) + o.Subject = u.ID.String() + o.Expiration = AccessTokenExpiry + + _, err = Sign(private, &o) is.NoErr(err) // access token - opt.Expiration = RefreshTokenExpiry - _, err = Sign(key.Private(), &opt) + o.Expiration = RefreshTokenExpiry + _, err = Sign(private, &o) is.NoErr(err) // refresh token }) } @@ -57,84 +54,20 @@ func TestMiddleware(t *testing.T) { t.Parallel() is := is.New(t) - t.Run("authenticate against Authorization header", func(t *testing.T) { - key := ES256() - - e := email.Email("foobar@gmail.com") - - opt := TokenOption{ - Issuer: "github.com/rog-golang-buddies/rmx", - Subject: suid.NewUUID().String(), - Expiration: time.Hour * 10, - Claims: []fp.Tuple{{"email", e.String()}}, - Algo: jwa.ES256, - } - - // ats - ats, err := Sign(key.Private(), &opt) - is.NoErr(err) // signing access token - - h := ParseAuth(opt.Algo, key.Public())(http.NotFoundHandler()) - - req, _ := http.NewRequest(http.MethodGet, "/", nil) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(ats))) - - res := httptest.NewRecorder() - - h.ServeHTTP(res, req) - is.Equal(res.Result().StatusCode, http.StatusNotFound) // http page not found - }) - - t.Run("authenticate against Cookie header", func(t *testing.T) { - key := ES256() - - e, cookieName := email.Email("foobar@gmail.com"), `__myCookie` - - opt := TokenOption{ - Issuer: "github.com/rog-golang-buddies/rmx", - Subject: suid.NewUUID().String(), - Expiration: time.Hour * 10, - Claims: []fp.Tuple{{"email", e.String()}}, - Algo: jwa.ES256, - } - - // rts - rts, err := Sign(key.Private(), &opt) - is.NoErr(err) // signing refresh token - - h := ParseAuth(opt.Algo, key.Public(), cookieName)(http.NotFoundHandler()) - - req, _ := http.NewRequest(http.MethodGet, "/", nil) - req.AddCookie(&http.Cookie{ - Path: "/", - Name: cookieName, - Value: string(rts), - HttpOnly: true, - SameSite: http.SameSiteLaxMode, - MaxAge: 24 * 7, - }) - - res := httptest.NewRecorder() - - h.ServeHTTP(res, req) - is.Equal(res.Result().StatusCode, http.StatusNotFound) // http page not found - }) - t.Run("jwk parse request", func(t *testing.T) { - key := ES256() + public, private := ES256() e, cookieName := email.Email("foobar@gmail.com"), `__g` - opt := TokenOption{ + o := TokenOption{ Issuer: "github.com/rog-golang-buddies/rmx", Subject: suid.NewUUID().String(), Expiration: time.Hour * 10, - Claims: []fp.Tuple{{"email", e.String()}}, - Algo: jwa.ES256, + Claims: map[string]any{"email": e.String()}, } // rts - rts, err := Sign(key.Private(), &opt) + rts, err := Sign(private, &o) is.NoErr(err) // signing refresh token c := &http.Cookie{ @@ -148,11 +81,8 @@ func TestMiddleware(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, "/", nil) req.AddCookie(c) - _, err = jwt.Parse( - []byte(c.Value), - jwt.WithKey(opt.Algo, key.Public()), - jwt.WithValidate(true), - ) + // + _, err = jwt.Parse([]byte(c.Value), jwt.WithKey(jwa.ES256, public), jwt.WithValidate(true)) is.NoErr(err) // parsing jwk page not found }) } diff --git a/internal/internal_test.go b/internal/internal_test.go index 5232809e..cde9ec37 100644 --- a/internal/internal_test.go +++ b/internal/internal_test.go @@ -5,9 +5,9 @@ import ( "strings" "testing" + "github.com/hyphengolang/prelude/testing/is" "github.com/hyphengolang/prelude/types/email" "github.com/hyphengolang/prelude/types/password" - "github.com/rog-golang-buddies/rmx/internal/is" ) func TestCustomTypes(t *testing.T) { diff --git a/internal/is/io.go b/internal/is/io.go deleted file mode 100644 index 86c774de..00000000 --- a/internal/is/io.go +++ /dev/null @@ -1,112 +0,0 @@ -package is - -import ( - "bufio" - "os" - "regexp" - "runtime" - "strings" -) - -const maxStackLen = 50 - -var reIsSourceFile = regexp.MustCompile(`is(-1.7)?\.go$`) - -func (is *I) callerInfo() (path string, line int, ok bool) { - var pc [maxStackLen]uintptr - // Skip two extra frames to account for this function - // and runtime.Callers itself. - n := runtime.Callers(2, pc[:]) - if n == 0 { - panic("is: zero callers found") - } - frames := runtime.CallersFrames(pc[:n]) - var firstFrame, frame runtime.Frame - for more := true; more; { - frame, more = frames.Next() - if reIsSourceFile.MatchString(frame.File) { - continue - } - if firstFrame.PC == 0 { - firstFrame = frame - } - if _, ok := is.helpers[frame.Function]; ok { - // Frame is inside a helper function. - continue - } - return frame.File, frame.Line, true - } - // If no "non-helper" frame is found, the first non is frame is returned. - return firstFrame.File, firstFrame.Line, true -} - -// loadArguments gets the arguments from the function call -// on the specified line of the file. -func loadArguments(path string, line int) (string, bool) { - f, err := os.Open(path) - if err != nil { - return "", false - } - defer f.Close() - s := bufio.NewScanner(f) - i := 1 - for s.Scan() { - if i != line { - i++ - continue - } - text := s.Text() - braceI := strings.Index(text, "(") - if braceI == -1 { - return "", false - } - text = text[braceI+1:] - cs := bufio.NewScanner(strings.NewReader(text)) - cs.Split(bufio.ScanBytes) - j := 0 - c := 1 - for cs.Scan() { - switch cs.Text() { - case ")": - c-- - case "(": - c++ - } - if c == 0 { - break - } - j++ - } - text = text[:j] - return text, true - } - return "", false -} - -// loadComment gets the Go comment from the specified line -// in the specified file. -func loadComment(path string, line int) (string, bool) { - f, err := os.Open(path) - if err != nil { - return "", false - } - defer f.Close() - s := bufio.NewScanner(f) - i := 1 - for s.Scan() { - if i != line { - i++ - continue - } - - text := s.Text() - commentI := strings.Index(text, "// ") - if commentI == -1 { - return "", false // no comment - } - text = text[commentI+2:] - text = strings.TrimSpace(text) - return text, true - } - return "", false -} diff --git a/internal/is/is.go b/internal/is/is.go deleted file mode 100644 index 48dcb007..00000000 --- a/internal/is/is.go +++ /dev/null @@ -1,157 +0,0 @@ -// NOTE the original always breaks for -// me so have taken one part -// of this, hopefully that is ok -// source: https://github.com/matryer/is - -package is - -import ( - "fmt" - "io" - "os" - "path/filepath" - "reflect" - "strings" -) - -type T interface { - Fail() - FailNow() -} - -// I is the test helper harness. -type I struct { - t T - out io.Writer - fail func() - helpers map[string]struct{} -} - -func New(t T) *I { - return &I{t, os.Stdout, t.FailNow, map[string]struct{}{}} -} - -func (is *I) NoErr(err error) { - if err != nil { - is.logf("err: %s", err.Error()) - } -} - -func (is *I) True(expression bool) { - if !expression { - is.log("not true: $ARGS") - } -} - -func (is *I) Equal(a, b any) { - if areEqual(a, b) { - return - } - // NOTE source: https://github.com/matryer/is/issues/46 - if isNil(a) || isNil(b) || reflect.ValueOf(a).Type() != reflect.ValueOf(b).Type() { - is.logf("%s != %s", is.valWithType(a), is.valWithType(b)) - } else { - is.logf("%v != %v", a, b) - } -} - -func (is *I) logf(format string, args ...interface{}) { - is.log(fmt.Sprintf(format, args...)) -} - -func (is *I) log(args ...interface{}) { - s := is.decorate(fmt.Sprint(args...)) - fmt.Fprint(os.Stdout, s) - is.fail() -} - -func (is *I) valWithType(v interface{}) string { - if isNil(v) { - return "" - } - return fmt.Sprintf("%[1]T(%[1]v)", v) -} - -// isNil gets whether the object is nil or not. -func isNil(object interface{}) bool { - if object == nil { - return true - } - value := reflect.ValueOf(object) - kind := value.Kind() - if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() { - return true - } - return false -} - -// areEqual gets whether a equals b or not. -func areEqual(a, b interface{}) bool { - // NOTE source: https://github.com/matryer/is/issues/49 - if isNil(a) || isNil(b) { - return isNil(a) && isNil(b) - } - - if reflect.DeepEqual(a, b) { - return true - } - aValue := reflect.ValueOf(a) - bValue := reflect.ValueOf(b) - return aValue == bValue -} - -// TODO add line and position number - https://github.com/matryer/is/blob/master/is.go -// -// decorate prefixes the string with the file and line of the call site -// and inserts the final newline if needed and indentation tabs for formatting. -// this function was copied from the testing framework and modified. -func (is *I) decorate(s string) string { - path, lineNumber, ok := is.callerInfo() // decorate + log + public function. - file := filepath.Base(path) - if ok { - // Truncate file name at last file name separator. - if index := strings.LastIndex(file, "/"); index >= 0 { - file = file[index+1:] - } else if index = strings.LastIndex(file, "\\"); index >= 0 { - file = file[index+1:] - } - } else { - file = "???" - lineNumber = 1 - } - - var sb strings.Builder - fmt.Fprintf(&sb, "%s:%d: ", file, lineNumber) // avoids needing to use strconv - s = escapeFormatString(s) - - lines := strings.Split(s, "\n") - if l := len(lines); l > 1 && lines[l-1] == "" { - lines = lines[:l-1] - } - - for i, line := range lines { - if i > 0 { - // Second and subsequent lines are indented an extra tab. - sb.WriteString("\n\t\t") - } - // expand arguments (if $ARGS is present) - if strings.Contains(line, "$ARGS") { - args, _ := loadArguments(path, lineNumber) - line = strings.Replace(line, "$ARGS", args, -1) - } - sb.WriteString(line) - } - comment, ok := loadComment(path, lineNumber) - if ok { - sb.WriteString(" // ") - comment = escapeFormatString(comment) - sb.WriteString(comment) - } - sb.WriteRune('\n') - return sb.String() -} - -// escapeFormatString escapes strings for use in formatted functions like Sprintf. -func escapeFormatString(fmt string) string { - return strings.Replace(fmt, "%", "%%", -1) -} diff --git a/service/auth/service_test.go b/service/auth/service_test.go index 4c95980f..0a5943a3 100644 --- a/service/auth/service_test.go +++ b/service/auth/service_test.go @@ -10,7 +10,7 @@ import ( "testing" "github.com/go-chi/chi/v5" - "github.com/rog-golang-buddies/rmx/internal/is" + "github.com/hyphengolang/prelude/testing/is" "github.com/rog-golang-buddies/rmx/store/auth" "github.com/rog-golang-buddies/rmx/store/sql/user" ) diff --git a/service/jam/service.go b/service/jam/service.go index e97b58b4..452a7703 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -237,11 +237,7 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeH func NewService(ctx context.Context, r chi.Router) *Service { s := &Service{ r, - ws.DefaultClient, - log.Print, - log.Printf, - h.Respond, - h.Decode, + ws.DefaultClient, log.Print, log.Printf, h.Respond, h.Decode, } s.routes() return s diff --git a/store/sql/user/repo_test.go b/store/sql/user/repo_test.go index 40d74726..916282de 100644 --- a/store/sql/user/repo_test.go +++ b/store/sql/user/repo_test.go @@ -4,11 +4,11 @@ import ( "context" "testing" + "github.com/hyphengolang/prelude/testing/is" "github.com/hyphengolang/prelude/types/email" "github.com/hyphengolang/prelude/types/password" "github.com/jackc/pgx/v5" "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/is" "github.com/rog-golang-buddies/rmx/internal/suid" ) From 2d71e89e699ebe2facb23e7b2ece07f59790dd29 Mon Sep 17 00:00:00 2001 From: aboublef Date: Thu, 20 Oct 2022 22:48:18 +0100 Subject: [PATCH 202/246] fix errors with key --- internal/auth/auth.go | 6 +++--- service/auth/service.go | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index f1ca9705..56daddd7 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -114,17 +114,17 @@ func ES256() (public, private jwk.Key) { panic(err) } - private, err := jwk.FromRaw(raw) + private, err = jwk.FromRaw(raw) if err != nil { panic(err) } - public, err := private.PublicKey() + public, err = private.PublicKey() if err != nil { panic(err) } - return &KeyPair{private, public} + return } func RS256() (public, private jwk.Key) { diff --git a/service/auth/service.go b/service/auth/service.go index 5a26b4a5..3f2216e4 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -74,18 +74,18 @@ type Service struct { } func (s *Service) routes() { - kp := auth.ES256() + public, private := auth.ES256() s.m.Route("/api/v1/auth", func(r chi.Router) { - r.Post("/sign-in", s.handleSignIn(kp.Private())) + r.Post("/sign-in", s.handleSignIn(private)) r.Delete("/sign-out", s.handleSignOut()) r.Post("/sign-up", s.handleSignUp()) - r.Get("/refresh", s.handleRefresh(kp.Public(), kp.Private())) + r.Get("/refresh", s.handleRefresh(public, private)) }) s.m.Route("/api/v1/account", func(r chi.Router) { - r.Get("/me", s.handleIdentity(kp.Public())) + r.Get("/me", s.handleIdentity(public)) }) } From a3f7a3289479e31dd10b5d86d9661e209a4ba40b Mon Sep 17 00:00:00 2001 From: aboublef Date: Fri, 21 Oct 2022 10:20:34 +0100 Subject: [PATCH 203/246] update to prelude@v0.1.1 --- go.mod | 2 +- go.sum | 4 ++-- store/sql/user/repo_test.go | 21 +++++++++++++-------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 32ca5303..69775bfe 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/go-chi/chi/v5 v5.0.7 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 - github.com/hyphengolang/prelude v0.1.0 + github.com/hyphengolang/prelude v0.1.1 github.com/lithammer/shortuuid/v4 v4.0.0 github.com/manifoldco/promptui v0.9.0 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index d4e1ef50..faa94404 100644 --- a/go.sum +++ b/go.sum @@ -45,8 +45,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hyphengolang/prelude v0.1.0 h1:Ml58zaIdC0H0pq7IxLYWl7yscX5QkJDSELz+2x6sxok= -github.com/hyphengolang/prelude v0.1.0/go.mod h1:O1Wj9q3gP0zJwsrLQKvE1hyVz9fZIwIsh+d7P8wOgOc= +github.com/hyphengolang/prelude v0.1.1 h1:L8JUlrdowWfcwzZKaVctdWdS7K4SiTVG+nJazOluPVY= +github.com/hyphengolang/prelude v0.1.1/go.mod h1:O1Wj9q3gP0zJwsrLQKvE1hyVz9fZIwIsh+d7P8wOgOc= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= diff --git a/store/sql/user/repo_test.go b/store/sql/user/repo_test.go index 916282de..4c382a68 100644 --- a/store/sql/user/repo_test.go +++ b/store/sql/user/repo_test.go @@ -2,6 +2,7 @@ package user import ( "context" + "os" "testing" "github.com/hyphengolang/prelude/testing/is" @@ -34,8 +35,11 @@ create temp table if not exists "user" ( commit; ` +var connStr = os.ExpandEnv("host=${POSTGRES_HOSTNAME} port=${DB_PORT} user=${POSTGRES_USER} password=${POSTGRES_PASSWORD} dbname=${POSTGRES_DB} sslmode=disable") + func init() { - c, err := pgx.Connect(context.Background(), `postgres://postgres:postgrespw@localhost:49153/postgres?sslmode=disable`) + + c, err := pgx.Connect(context.Background(), connStr) if err != nil { panic(err) } @@ -60,11 +64,12 @@ func TestPSQL(t *testing.T) { }) t.Run(`insert two new users`, func(t *testing.T) { + fizz := internal.User{ ID: suid.NewUUID(), - Email: "fizz@mail.com", + Email: email.MustParse("fizz@mail.com"), Username: "fizz", - Password: password.Password("fizz_pw_1").MustHash(), + Password: password.MustParse("fizz_pw_1").MustHash(), } err := db.Insert(ctx, &fizz) @@ -72,9 +77,9 @@ func TestPSQL(t *testing.T) { buzz := internal.User{ ID: suid.NewUUID(), - Email: "buzz@mail.com", + Email: email.MustParse("buzz@mail.com"), Username: "buzz", - Password: password.Password("buzz_pw_1").MustHash(), + Password: password.MustParse("buzz_pw_1").MustHash(), } err = db.Insert(ctx, &buzz) @@ -88,9 +93,9 @@ func TestPSQL(t *testing.T) { t.Run("reject user with duplicate email/username", func(t *testing.T) { fizz := internal.User{ ID: suid.NewUUID(), - Email: "fuzz@mail.com", + Email: email.MustParse("fuzz@mail.com"), Username: "fizz", - Password: password.Password("fuzz_pw_1").MustHash(), + Password: password.MustParse("fuzz_pw_1").MustHash(), } err := db.Insert(ctx, &fizz) @@ -102,7 +107,7 @@ func TestPSQL(t *testing.T) { is.NoErr(err) // select user where username = "fizz" is.NoErr(u.Password.Compare("fizz_pw_1")) // valid login - _, err = db.Select(ctx, email.Email("buzz@mail.com")) + _, err = db.Select(ctx, email.MustParse("buzz@mail.com")) is.NoErr(err) // select user where email = "buzz@mail.com" }) From 729b507d5873b271d534f2b00121a77982c84eec Mon Sep 17 00:00:00 2001 From: aboublef Date: Fri, 21 Oct 2022 10:51:03 +0100 Subject: [PATCH 204/246] added experimental ws package --- internal/websocket/x/conn.go | 169 +++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 internal/websocket/x/conn.go diff --git a/internal/websocket/x/conn.go b/internal/websocket/x/conn.go new file mode 100644 index 00000000..13ce3383 --- /dev/null +++ b/internal/websocket/x/conn.go @@ -0,0 +1,169 @@ +package websocket + +import ( + "context" + "encoding/json" + "io" + "net" + "net/http" + + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" +) + +type Conn interface { + Reader + Writer +} + +type Reader interface { + Close() error + State() ws.State + + Read() ([]byte, error) + ReadString() (string, error) + ReadJSON(v any) error +} + +type Writer interface { + Close() error + State() ws.State + + Write(p []byte) error + WriteString(p string) error + WriteJSON(v any) error +} + +type serverConn struct { + r *wsutil.Reader + w *wsutil.Writer + + rwc net.Conn +} + +func (c *serverConn) State() ws.State { return ws.StateServerSide } + +func (c *serverConn) Close() error { return c.rwc.Close() } + +func (c *serverConn) Read() ([]byte, error) { + b, err := wsutil.ReadClientBinary(c.rwc) + return b, err +} + +func (c *serverConn) ReadString() (string, error) { + h, err := c.r.NextFrame() + if err != nil { + return "", err + } + + // Reset writer to write frame with right operation code. + c.w.Reset(c.rwc, c.State(), h.OpCode) + + b, err := io.ReadAll(c.r) + return string(b), err +} + +func (c *serverConn) ReadJSON(v any) error { + h, err := c.r.NextFrame() + if err != nil { + return err + } + + if h.OpCode == ws.OpClose { + return io.EOF + } + return json.NewDecoder(c.r).Decode(v) +} + +func (c *serverConn) Write(p []byte) error { + return wsutil.WriteMessage(c.rwc, c.State(), ws.OpBinary, p) +} + +func (c *serverConn) WriteString(s string) error { + _, err := io.WriteString(c.w, s) + if err != nil { + return err + } + return c.w.Flush() +} + +func (c *serverConn) WriteJSON(v any) error { + if err := json.NewEncoder(c.w).Encode(v); err != nil { + return err + } + return c.w.Flush() +} + +func UpgradeHTTP(w http.ResponseWriter, r *http.Request) (conn Conn, err error) { + rwc, _, _, err := ws.UpgradeHTTP(r, w) + c := &serverConn{rwc: rwc} + c.r = wsutil.NewReader(rwc, c.State()) + c.w = wsutil.NewWriter(rwc, c.State(), ws.OpText) + return c, err +} + +type clientConn struct { + r *wsutil.Reader + w *wsutil.Writer + + rwc net.Conn +} + +func (c *clientConn) State() ws.State { return ws.StateClientSide } + +func (c *clientConn) Close() error { return c.rwc.Close() } + +func (c *clientConn) Read() ([]byte, error) { + b, err := wsutil.ReadServerBinary(c.rwc) + return b, err +} + +func (c *clientConn) ReadString() (string, error) { + h, err := c.r.NextFrame() + if err != nil { + return "", err + } + // Reset writer to write frame with right operation code. + c.w.Reset(c.rwc, c.State(), h.OpCode) + + b, err := io.ReadAll(c.r) + return string(b), err +} + +func (c *clientConn) ReadJSON(v any) error { + h, err := c.r.NextFrame() + if err != nil { + return err + } + if h.OpCode == ws.OpClose { + return io.EOF + } + return json.NewDecoder(c.r).Decode(v) +} + +func (c *clientConn) Write(p []byte) error { + return wsutil.WriteMessage(c.rwc, c.State(), ws.OpBinary, p) +} + +func (c *clientConn) WriteString(s string) error { + _, err := io.WriteString(c.w, s) + if err != nil { + return err + } + return c.w.Flush() +} + +func (c *clientConn) WriteJSON(v any) error { + if err := json.NewEncoder(c.w).Encode(v); err != nil { + return err + } + return c.w.Flush() +} + +func Dial(ctx context.Context, urlStr string) (conn Conn, err error) { + rwc, _, _, err := ws.Dial(context.Background(), urlStr) + c := &clientConn{rwc: rwc} + c.r = wsutil.NewReader(rwc, c.State()) + c.w = wsutil.NewWriter(rwc, c.State(), ws.OpText) + return c, err +} From ef203ef0c7ece37a9fbca374f9c5d4c40f4ecac7 Mon Sep 17 00:00:00 2001 From: aboublef Date: Fri, 21 Oct 2022 10:51:39 +0100 Subject: [PATCH 205/246] echo test with websocket --- service/jam/service.go | 26 +++++++++++++++++++++++--- service/jam/service_test.go | 31 ++++++++++++++++++++----------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/service/jam/service.go b/service/jam/service.go index 452a7703..77ae2b01 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -16,6 +16,7 @@ import ( "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/suid" ws "github.com/rog-golang-buddies/rmx/internal/websocket" + w2 "github.com/rog-golang-buddies/rmx/internal/websocket/x" ) func (s *Service) routes() { @@ -25,10 +26,29 @@ func (s *Service) routes() { r.Get("/{uuid}", s.handleGetRoom()) }) - s.m.Route("/ws/jam", func(r chi.Router) { - r = r.With(s.connectionPool(nil), s.upgradeHTTP(1024, 1024)) - r.Get("/{uuid}", s.handleP2PComms()) + s.m.Route("/ws", func(r chi.Router) { + r.Route("/echo", func(r chi.Router) { + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + conn, err := w2.UpgradeHTTP(w, r) + if err != nil { + s.respond(w, r, err, http.StatusUpgradeRequired) + return + } + + defer conn.Close() + for { + msg, _ := conn.ReadString() + _ = conn.WriteString(msg) + } + }) + }) + + r.Route("/jam", func(r chi.Router) { + r = r.With(s.connectionPool(nil), s.upgradeHTTP(1024, 1024)) + r.Get("/{uuid}", s.handleP2PComms()) + }) }) + } func (s *Service) handleP2PComms() http.HandlerFunc { diff --git a/service/jam/service_test.go b/service/jam/service_test.go index 103568ec..4e9bd5c2 100644 --- a/service/jam/service_test.go +++ b/service/jam/service_test.go @@ -3,26 +3,35 @@ package jam import ( "context" "net/http/httptest" + "strings" "testing" "github.com/go-chi/chi/v5" + "github.com/hyphengolang/prelude/testing/is" + w2 "github.com/rog-golang-buddies/rmx/internal/websocket/x" // "github.com/rog-golang-buddies/rmx/internal/websocket" ) func TestRoutes(t *testing.T) { - srv := NewService(context.Background(), chi.NewMux()) + t.Parallel() + is := is.New(t) - // srv.r.Get("/ws/echo", chain(srv.handleEcho(),srv.upgradeHTTP(1024,1024),srv.connectionPool(websocket.DefaultPool()))) + h := NewService(context.Background(), chi.NewMux()) + srv := httptest.NewServer(h) - s := httptest.NewServer(srv) - t.Cleanup(func() { s.Close() }) + // setup websocket + conn, err := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/echo") + is.NoErr(err) // dial error - r, err := s.Client().Get(s.URL + "/api/v1/jam/ping") - if err != nil { - t.Fatal(err) - } + t.Cleanup(func() { conn.Close(); srv.Close() }) - if r.StatusCode != 204 { - t.Fatalf("expected %d;got %d", 204, r.StatusCode) - } + t.Run(`connect to echo server`, func(t *testing.T) { + err := conn.WriteString("Hello, World") + is.NoErr(err) // write string to server + + msg, err := conn.ReadString() + is.NoErr(err) // read string from server + + is.Equal(msg, "Hello, World") // server message == client message + }) } From bfda18b9b268a1e0e0160e44df0400a9732d4d69 Mon Sep 17 00:00:00 2001 From: aboublef Date: Fri, 21 Oct 2022 10:52:08 +0100 Subject: [PATCH 206/246] update external pacakges --- go.mod | 3 +++ go.sum | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/go.mod b/go.mod index 69775bfe..e99326fb 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/charmbracelet/lipgloss v0.6.0 github.com/go-chi/chi v1.5.4 github.com/go-chi/chi/v5 v5.0.7 + github.com/gobwas/ws v1.1.0 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 github.com/hyphengolang/prelude v0.1.1 @@ -26,6 +27,8 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect github.com/goccy/go-json v0.9.11 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect diff --git a/go.sum b/go.sum index faa94404..2e11092a 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,12 @@ github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-redis/redis/v9 v9.0.0-beta.2 h1:ZSr84TsnQyKMAg8gnV+oawuQezeJR11/09THcWCQzr4= github.com/go-redis/redis/v9 v9.0.0-beta.2/go.mod h1:Bldcd/M/bm9HbnNPi/LUtYBSD8ttcZYBMupwMXhdU0o= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= +github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= @@ -137,6 +143,7 @@ golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ff golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From e4c7b4cb08c360f0421c8c8b18cea9f580046ab5 Mon Sep 17 00:00:00 2001 From: aboublef Date: Fri, 21 Oct 2022 12:06:58 +0100 Subject: [PATCH 207/246] tests between two clients using a string message --- service/jam/service.go | 46 ++++++++++++++++++++++++++++++++++--- service/jam/service_test.go | 37 +++++++++++++++++++++-------- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/service/jam/service.go b/service/jam/service.go index 77ae2b01..c28f10d1 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -6,6 +6,7 @@ import ( "errors" "log" "net/http" + "sync" "github.com/go-chi/chi/v5" "github.com/gorilla/websocket" @@ -19,6 +20,34 @@ import ( w2 "github.com/rog-golang-buddies/rmx/internal/websocket/x" ) +type Pool struct { + mu sync.Mutex + m map[w2.Conn]bool +} + +func (p *Pool) BroadcastString(s string) error { + p.mu.Lock() + defer p.mu.Unlock() + + for c := range p.m { + if err := c.WriteString(s); err != nil { + return err + } + } + + return nil +} + +func (p *Pool) remove(c w2.Conn) error { + p.mu.Lock() + defer p.mu.Unlock() + if err := c.Close(); err != nil { + return err + } + delete(p.m, c) + return nil +} + func (s *Service) routes() { s.m.Route("/api/v1/jam", func(r chi.Router) { r.Get("/", s.handleListRooms()) @@ -27,6 +56,9 @@ func (s *Service) routes() { }) s.m.Route("/ws", func(r chi.Router) { + + pool := Pool{m: make(map[w2.Conn]bool)} + r.Route("/echo", func(r chi.Router) { r.Get("/", func(w http.ResponseWriter, r *http.Request) { conn, err := w2.UpgradeHTTP(w, r) @@ -35,10 +67,18 @@ func (s *Service) routes() { return } - defer conn.Close() + pool.m[conn] = true + + defer pool.remove(conn) for { - msg, _ := conn.ReadString() - _ = conn.WriteString(msg) + msg, err := conn.ReadString() + if err != nil { + return + } + // s.log("connection added") + if err := pool.BroadcastString(msg); err != nil { + return + } } }) }) diff --git a/service/jam/service_test.go b/service/jam/service_test.go index 4e9bd5c2..aa9a8fd8 100644 --- a/service/jam/service_test.go +++ b/service/jam/service_test.go @@ -12,26 +12,45 @@ import ( // "github.com/rog-golang-buddies/rmx/internal/websocket" ) -func TestRoutes(t *testing.T) { +func TestMultipleClients(t *testing.T) { t.Parallel() is := is.New(t) h := NewService(context.Background(), chi.NewMux()) srv := httptest.NewServer(h) - // setup websocket - conn, err := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/echo") - is.NoErr(err) // dial error - - t.Cleanup(func() { conn.Close(); srv.Close() }) + t.Cleanup(func() { srv.Close() }) t.Run(`connect to echo server`, func(t *testing.T) { - err := conn.WriteString("Hello, World") + c1, err := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/echo") + is.NoErr(err) // dial error + + err = c1.WriteString("Hello, World!") is.NoErr(err) // write string to server - msg, err := conn.ReadString() + msg, err := c1.ReadString() is.NoErr(err) // read string from server - is.Equal(msg, "Hello, World") // server message == client message + is.Equal(msg, "Hello, World!") // server message == client message + + c1.Close() + }) + + t.Run(`communicate between two connections`, func(t *testing.T) { + c2, _ := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/echo") + c3, _ := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/echo") + + err := c2.WriteString("Hello, World!") + is.NoErr(err) // write string to pool + + // time.Sleep(time.Second * 1) + + msg, err := c3.ReadString() + is.NoErr(err) // read message sent by c1 + + is.Equal(msg, "Hello, World!") + + c2.Close() + c3.Close() }) } From 54dd199daf0128ac881e1e518799b27f3b529b4a Mon Sep 17 00:00:00 2001 From: pmoieni Date: Wed, 2 Nov 2022 00:50:10 +0330 Subject: [PATCH 208/246] =?UTF-8?q?=F0=9F=A7=AC=20small=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/main.go | 10 ++++------ internal/commands/commands.go | 9 +++++++++ internal/commands/run.go | 9 ++++++++- service/service.go | 2 +- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 34d90b8d..3bb8e487 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -20,13 +20,11 @@ func initCLI() *cli.App { EnableBashCompletion: true, Name: "rmx", Usage: "RapidMidiEx Server CLI", - Version: "v0.0.1", + Version: commands.Version, Compiled: time.Now().UTC(), - Action: func(*cli.Context) error { - return nil - }, - Flags: commands.Flags, - Commands: commands.Commands, + Action: commands.GetVersion, + Flags: commands.Flags, + Commands: commands.Commands, } return c diff --git a/internal/commands/commands.go b/internal/commands/commands.go index ae96c318..d279ddda 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -2,6 +2,7 @@ package commands import ( "errors" + "fmt" "github.com/urfave/cli/v2" "github.com/urfave/cli/v2/altsrc" @@ -44,3 +45,11 @@ var Commands = []*cli.Command{ Flags: Flags, }, } + +// shouldn't be here +const Version = "v0.0.0-a.1" + +func GetVersion(cCtx *cli.Context) error { + _, err := fmt.Println("rmx version: " + Version) + return err +} diff --git a/internal/commands/run.go b/internal/commands/run.go index 04700ddf..035ccbb9 100644 --- a/internal/commands/run.go +++ b/internal/commands/run.go @@ -14,6 +14,8 @@ import ( "github.com/manifoldco/promptui" "github.com/rog-golang-buddies/rmx/config" + "github.com/rog-golang-buddies/rmx/service" + "github.com/rog-golang-buddies/rmx/store" "github.com/rs/cors" "github.com/urfave/cli/v2" "golang.org/x/sync/errgroup" @@ -255,9 +257,14 @@ func serve(cfg *config.Config) error { AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, } + // init application store + s := store.New(sCtx, "") // needs fix + // setup a new handler + h := service.New(sCtx, s) + srv := http.Server{ Addr: ":" + cfg.ServerPort, - Handler: cors.New(c).Handler(http.NotFoundHandler()), + Handler: cors.New(c).Handler(h), // max time to read request from the client ReadTimeout: 10 * time.Second, // max time to write response to the client diff --git a/service/service.go b/service/service.go index f84545db..4767f3fb 100644 --- a/service/service.go +++ b/service/service.go @@ -27,7 +27,7 @@ type Service struct { func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func New(ctx context.Context, st store.Store) http.Handler { +func New(ctx context.Context, st *store.Store) http.Handler { s := &Service{chi.NewMux(), log.Print, log.Printf, log.Fatal, log.Fatalf} s.routes() From 85c062e0005d4b746e5b695096f1a65e89de4104 Mon Sep 17 00:00:00 2001 From: aboublef Date: Sat, 29 Oct 2022 19:13:27 +0100 Subject: [PATCH 209/246] rework with user repo --- internal/internal.go | 25 ++++++-- store/sql/user/repo.go | 121 ++++++++++++++++++------------------ store/sql/user/repo_test.go | 20 +++--- 3 files changed, 92 insertions(+), 74 deletions(-) diff --git a/internal/internal.go b/internal/internal.go index 95428873..7b66e416 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -111,13 +111,23 @@ type WTokenClient interface { BlackListRefreshToken(ctx context.Context, token string) error } +type RWUserRepo interface { + UserCloser + UserReader + UserWriter +} + +type WUserRepo interface { + UserCloser + UserWriter +} + type UserRepo interface { - RUserRepo - WUserRepo + UserCloser + UserReader } -type RUserRepo interface { - Close(ctx context.Context) error +type UserReader interface { // Returns an array of users subject to any filter // conditions that are required SelectMany(ctx context.Context) ([]User, error) @@ -127,8 +137,7 @@ type RUserRepo interface { Select(ctx context.Context, key any) (*User, error) } -type WUserRepo interface { - Close(ctx context.Context) error +type UserWriter interface { // Insert a new user to the database Insert(ctx context.Context, u *User) error // Performs a "hard" delete from database @@ -136,6 +145,10 @@ type WUserRepo interface { Delete(ctx context.Context, key any) error } +type UserCloser interface { + Close() +} + // Custom user type required type User struct { ID suid.UUID `json:"id"` diff --git a/store/sql/user/repo.go b/store/sql/user/repo.go index dc233c20..1b5abf08 100644 --- a/store/sql/user/repo.go +++ b/store/sql/user/repo.go @@ -11,11 +11,27 @@ import ( "github.com/hyphengolang/prelude/types/password" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/suid" ) +const ( + qryInsert = `insert into "user" (id, email, username, password) values (@id, @email, @username, @password)` + + qrySelectMany = `select id, email, username, password from "user" order by id` + + qrySelectByID = `select id, email, username, password from "user" where id = $1` + qrySelectByEmail = `select id, email, username, password from "user" where email = $1` + qrySelectByUsername = `select id, email, username, password from "user" where username = $1` + + qryDeleteByID = `delete from "user" where id = $1` + qryDeleteByEmail = `delete from "user" where email = $1` + qryDeleteByUsername = `delete from "user" where username = $1` +) + +// Definition of our User in the DB layer type User struct { // Primary key. ID suid.UUID @@ -35,39 +51,24 @@ type User struct { type Repo struct { ctx context.Context + c *pgxpool.Pool +} - c *pgx.Conn +// This is not really required +func NewRepo(ctx context.Context, conn *pgxpool.Pool) *Repo { + return &Repo{ctx, conn} } -func (r *Repo) Context() context.Context { - if r.ctx != nil { - return r.ctx +func (s *Repo) Context() context.Context { + if s.ctx != nil { + return s.ctx } return context.Background() } -func (r *Repo) Close(ctx context.Context) error { return r.c.Close(ctx) } - -func NewRepo(ctx context.Context, conn *pgx.Conn) internal.UserRepo { - r := &Repo{ctx, conn} - return r -} - -const ( - qryInsert = `insert into "user" (id, email, username, password) values (@id, @email, @username, @password)` - - qrySelectMany = `select id, email, username, password from "user" order by id` - - qrySelectByID = `select id, email, username, password from "user" where id = $1` - qrySelectByEmail = `select id, email, username, password from "user" where email = $1` - qrySelectByUsername = `select id, email, username, password from "user" where username = $1` - - qryDeleteByID = `delete from "user" where id = $1` - qryDeleteByEmail = `delete from "user" where email = $1` - qryDeleteByUsername = `delete from "user" where username = $1` -) +func (s *Repo) Close() { s.c.Close() } -func (r *Repo) Insert(ctx context.Context, u *internal.User) error { +func (s *Repo) Insert(ctx context.Context, u *internal.User) error { args := pgx.NamedArgs{ "id": u.ID, "email": u.Email, @@ -75,16 +76,16 @@ func (r *Repo) Insert(ctx context.Context, u *internal.User) error { "password": u.Password, } - return psql.Exec(r.c, qryInsert, args) + return psql.Exec(s.c, qryInsert, args) } -func (r *Repo) SelectMany(ctx context.Context) ([]internal.User, error) { - return psql.Query(r.c, qrySelectMany, func(r pgx.Rows, u *internal.User) error { +func (s *Repo) SelectMany(ctx context.Context) ([]internal.User, error) { + return psql.Query(s.c, qrySelectMany, func(r pgx.Rows, u *internal.User) error { return r.Scan(&u.ID, &u.Email, &u.Username, &u.Password) }) } -func (r *Repo) Select(ctx context.Context, key any) (*internal.User, error) { +func (s *Repo) Select(ctx context.Context, key any) (*internal.User, error) { var qry string switch key.(type) { case suid.UUID: @@ -97,10 +98,10 @@ func (r *Repo) Select(ctx context.Context, key any) (*internal.User, error) { return nil, internal.ErrInvalidType } var u internal.User - return &u, psql.QueryRow(r.c, qry, func(r pgx.Row) error { return r.Scan(&u.ID, &u.Username, &u.Email, &u.Password) }, key) + return &u, psql.QueryRow(s.c, qry, func(r pgx.Row) error { return r.Scan(&u.ID, &u.Username, &u.Email, &u.Password) }, key) } -func (r *Repo) Delete(ctx context.Context, key any) error { +func (s *Repo) Delete(ctx context.Context, key any) error { var qry string switch key.(type) { case suid.UUID: @@ -112,21 +113,23 @@ func (r *Repo) Delete(ctx context.Context, key any) error { default: return internal.ErrInvalidType } - return psql.Exec(r.c, qry, key) + return psql.Exec(s.c, qry, key) } -var DefaultRepo = &repo{ +var DefaultRepo = &store{ miu: make(map[suid.UUID]*User), mei: make(map[string]*User), log: log.Println, logf: log.Printf, } -func (r *repo) Close(ctx context.Context) error { return nil } +// NOTE in-memory implementation not required anymore + +func (s *store) Close(ctx context.Context) error { return nil } -func (r *repo) Delete(ctx context.Context, key any) error { return nil } +func (s *store) Delete(ctx context.Context, key any) error { return nil } -type repo struct { +type store struct { mu sync.Mutex miu map[suid.UUID]*User mei map[string]*User @@ -136,15 +139,15 @@ type repo struct { } // Remove implements internal.UserRepo -func (r *repo) Remove(ctx context.Context, key any) error { +func (s *store) Remove(ctx context.Context, key any) error { panic("unimplemented") } -func (r *repo) Insert(ctx context.Context, iu *internal.User) error { - r.mu.Lock() - defer r.mu.Unlock() +func (s *store) Insert(ctx context.Context, iu *internal.User) error { + s.mu.Lock() + defer s.mu.Unlock() - if _, found := r.mei[iu.Email.String()]; found { + if _, found := s.mei[iu.Email.String()]; found { return internal.ErrAlreadyExists } @@ -155,33 +158,33 @@ func (r *repo) Insert(ctx context.Context, iu *internal.User) error { Password: iu.Password, CreatedAt: time.Now(), } - r.mei[iu.Email.String()], r.miu[iu.ID] = u, u + s.mei[iu.Email.String()], s.miu[iu.ID] = u, u return nil } -func (r *repo) SelectMany(ctx context.Context) ([]internal.User, error) { +func (s *store) SelectMany(ctx context.Context) ([]internal.User, error) { panic("not implemented") } -func (r *repo) Select(ctx context.Context, key any) (*internal.User, error) { +func (s *store) Select(ctx context.Context, key any) (*internal.User, error) { switch key := key.(type) { case suid.UUID: - return r.selectUUID(key) + return s.selectUUID(key) case email.Email: - return r.selectEmail(key) + return s.selectEmail(key) case string: - return r.selectUsername(key) + return s.selectUsername(key) default: return nil, internal.ErrInvalidType } } -func (r *repo) selectUUID(uid suid.UUID) (*internal.User, error) { - r.mu.Lock() - defer r.mu.Unlock() +func (s *store) selectUUID(uid suid.UUID) (*internal.User, error) { + s.mu.Lock() + defer s.mu.Unlock() - if u, ok := r.miu[uid]; ok { + if u, ok := s.miu[uid]; ok { return &internal.User{ ID: u.ID, Username: u.Username, @@ -193,11 +196,11 @@ func (r *repo) selectUUID(uid suid.UUID) (*internal.User, error) { return nil, internal.ErrNotFound } -func (r *repo) selectUsername(username string) (*internal.User, error) { - r.mu.Lock() - defer r.mu.Unlock() +func (s *store) selectUsername(username string) (*internal.User, error) { + s.mu.Lock() + defer s.mu.Unlock() - for _, u := range r.mei { + for _, u := range s.mei { if u.Username == username { return &internal.User{ ID: u.ID, @@ -211,11 +214,11 @@ func (r *repo) selectUsername(username string) (*internal.User, error) { return nil, internal.ErrNotFound } -func (r *repo) selectEmail(email email.Email) (*internal.User, error) { - r.mu.Lock() - defer r.mu.Unlock() +func (s *store) selectEmail(email email.Email) (*internal.User, error) { + s.mu.Lock() + defer s.mu.Unlock() - if u, ok := r.mei[email.String()]; ok { + if u, ok := s.mei[email.String()]; ok { return &internal.User{ ID: u.ID, Username: u.Username, diff --git a/store/sql/user/repo_test.go b/store/sql/user/repo_test.go index 4c382a68..f39c0419 100644 --- a/store/sql/user/repo_test.go +++ b/store/sql/user/repo_test.go @@ -2,13 +2,13 @@ package user import ( "context" - "os" "testing" + psql "github.com/hyphengolang/prelude/sql/postgres" "github.com/hyphengolang/prelude/testing/is" "github.com/hyphengolang/prelude/types/email" "github.com/hyphengolang/prelude/types/password" - "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/suid" ) @@ -16,7 +16,7 @@ import ( /* https://www.covermymeds.com/main/insights/articles/on-update-timestamps-mysql-vs-postgres/ */ -var db internal.UserRepo +var db internal.RWUserRepo const migration = ` begin; @@ -35,20 +35,22 @@ create temp table if not exists "user" ( commit; ` -var connStr = os.ExpandEnv("host=${POSTGRES_HOSTNAME} port=${DB_PORT} user=${POSTGRES_USER} password=${POSTGRES_PASSWORD} dbname=${POSTGRES_DB} sslmode=disable") +var pool *pgxpool.Pool +var err error func init() { - - c, err := pgx.Connect(context.Background(), connStr) + // create pool connection + pool, err = pgxpool.New(context.Background(), `postgres://postgres:postgrespw@localhost:49153/testing`) if err != nil { panic(err) } - if _, err := c.Exec(context.Background(), migration); err != nil { + // setup migration + if err := psql.Exec(pool, migration); err != nil { panic(err) } - db = NewRepo(context.Background(), c) + db = NewRepo(context.Background(), pool) } func TestPSQL(t *testing.T) { @@ -56,7 +58,7 @@ func TestPSQL(t *testing.T) { is, ctx := is.New(t), context.Background() - t.Cleanup(func() { db.Close(ctx) }) + t.Cleanup(func() { pool.Close() }) t.Run(`select * from "user"`, func(t *testing.T) { _, err := db.SelectMany(ctx) From 08731a1c8891f65aa1b39404f655c290de6c4859 Mon Sep 17 00:00:00 2001 From: aboublef Date: Sat, 29 Oct 2022 19:18:08 +0100 Subject: [PATCH 210/246] fix user repo bugs --- service/auth/service.go | 4 ++-- store/sql/user/repo.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/service/auth/service.go b/service/auth/service.go index 3f2216e4..0d431a5a 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -61,7 +61,7 @@ type Service struct { m chi.Router - r internal.UserRepo + r internal.RWUserRepo tc internal.TokenClient log func(...any) @@ -342,7 +342,7 @@ func (s *Service) signedTokens(private jwk.Key, u *internal.User) (its, ats, rts func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } -func NewService(ctx context.Context, m chi.Router, r internal.UserRepo, tc internal.TokenClient) *Service { +func NewService(ctx context.Context, m chi.Router, r internal.RWUserRepo, tc internal.TokenClient) *Service { s := &Service{ctx, m, r, tc, log.Println, log.Printf, h.Decode, h.Respond, h.Created, http.SetCookie} s.routes() return s diff --git a/store/sql/user/repo.go b/store/sql/user/repo.go index 1b5abf08..ed4d918d 100644 --- a/store/sql/user/repo.go +++ b/store/sql/user/repo.go @@ -116,6 +116,8 @@ func (s *Repo) Delete(ctx context.Context, key any) error { return psql.Exec(s.c, qry, key) } +// NOTE in-memory implementation not required anymore + var DefaultRepo = &store{ miu: make(map[suid.UUID]*User), mei: make(map[string]*User), @@ -123,9 +125,7 @@ var DefaultRepo = &store{ logf: log.Printf, } -// NOTE in-memory implementation not required anymore - -func (s *store) Close(ctx context.Context) error { return nil } +func (s *store) Close() {} func (s *store) Delete(ctx context.Context, key any) error { return nil } From 336cd4a0050af1c0e8e6373d5672c1f46f5dd560 Mon Sep 17 00:00:00 2001 From: aboublef Date: Sat, 29 Oct 2022 22:25:30 +0100 Subject: [PATCH 211/246] progress with concurrent pool --- internal/websocket/x/README.md | 3 + internal/websocket/x/client.go | 45 ++++++ internal/websocket/x/pool/pool.go | 72 +++++++++ .../websocket/x/{conn.go => websocket.go} | 18 ++- internal/websocket/x/websocket_test.go | 1 + service/jam/service.go | 144 ++++-------------- service/jam/service_test.go | 41 ++--- 7 files changed, 186 insertions(+), 138 deletions(-) create mode 100644 internal/websocket/x/README.md create mode 100644 internal/websocket/x/client.go create mode 100644 internal/websocket/x/pool/pool.go rename internal/websocket/x/{conn.go => websocket.go} (95%) create mode 100644 internal/websocket/x/websocket_test.go diff --git a/internal/websocket/x/README.md b/internal/websocket/x/README.md new file mode 100644 index 00000000..06a36c85 --- /dev/null +++ b/internal/websocket/x/README.md @@ -0,0 +1,3 @@ +# Websockets + +Inspiration taken from [here](https://medium.com/free-code-camp/million-websockets-and-go-cc58418460bb) diff --git a/internal/websocket/x/client.go b/internal/websocket/x/client.go new file mode 100644 index 00000000..0b213d46 --- /dev/null +++ b/internal/websocket/x/client.go @@ -0,0 +1,45 @@ +package websocket + +import ( + "sync" +) + +type Pool struct { + Capacity uint + + seq uint + cs sync.Map +} + +func NewPool(cap uint) *Pool { + return &Pool{Capacity: cap} +} + +func (p *Pool) Append(conn Conn) { + p.cs.Store(conn, struct{}{}) + p.seq++ + +} + +func (p *Pool) IsCap() bool { return p.seq >= p.Capacity } + +func (p *Pool) Remove(conn Conn) error { + p.cs.Delete(conn) + p.seq-- + return conn.Close() +} + +func (p *Pool) Broadcast(msg any) { + var f func(key, value any) bool + + switch msg := msg.(type) { + case []byte: + f = func(key, value any) bool { return key.(Conn).Write(msg) == nil } + case string: + f = func(key, value any) bool { return key.(Conn).WriteString(msg) == nil } + default: + f = func(key, value any) bool { return key.(Conn).WriteJSON(msg) == nil } + } + + p.cs.Range(f) +} diff --git a/internal/websocket/x/pool/pool.go b/internal/websocket/x/pool/pool.go new file mode 100644 index 00000000..6e22b011 --- /dev/null +++ b/internal/websocket/x/pool/pool.go @@ -0,0 +1,72 @@ +package pool + +import ( + "fmt" + "time" +) + +// ErrScheduleTimeout returned by Pool to indicate that there no free +// goroutines during some period of time. +var ErrScheduleTimeout = fmt.Errorf("schedule error: timed out") + +// Pool contains logic of goroutine reuse. +type Pool struct { + sem chan struct{} + work chan func() +} + +// NewPool creates new goroutine pool with given size. It also creates a work +// queue of given size. Finally, it spawns given amount of goroutines +// immediately. +func NewPool(size, queue, spawn int) *Pool { + if spawn <= 0 && queue > 0 { + panic("dead queue configuration detected") + } + + if spawn > size { + panic("spawn > workers") + } + p := &Pool{ + sem: make(chan struct{}, size), + work: make(chan func(), queue), + } + for i := 0; i < spawn; i++ { + p.sem <- struct{}{} + go p.worker(func() {}) + } + + return p +} + +// Schedule schedules task to be executed over pool's workers. +func (p *Pool) Schedule(task func()) { + p.schedule(task, nil) +} + +// ScheduleTimeout schedules task to be executed over pool's workers. +// It returns ErrScheduleTimeout when no free workers met during given timeout. +func (p *Pool) ScheduleTimeout(timeout time.Duration, task func()) error { + return p.schedule(task, time.After(timeout)) +} + +func (p *Pool) schedule(task func(), timeout <-chan time.Time) error { + select { + case <-timeout: + return ErrScheduleTimeout + case p.work <- task: + return nil + case p.sem <- struct{}{}: + go p.worker(task) + return nil + } +} + +func (p *Pool) worker(task func()) { + defer func() { <-p.sem }() + + task() + + for task := range p.work { + task() + } +} diff --git a/internal/websocket/x/conn.go b/internal/websocket/x/websocket.go similarity index 95% rename from internal/websocket/x/conn.go rename to internal/websocket/x/websocket.go index 13ce3383..55ab83f6 100644 --- a/internal/websocket/x/conn.go +++ b/internal/websocket/x/websocket.go @@ -11,13 +11,29 @@ import ( "github.com/gobwas/ws/wsutil" ) +// Connection + type Conn interface { Reader Writer + Closer } -type Reader interface { +type ReaderCloser interface { + Reader + Closer +} + +type WriterCloser interface { + Writer + Closer +} + +type Closer interface { Close() error +} + +type Reader interface { State() ws.State Read() ([]byte, error) diff --git a/internal/websocket/x/websocket_test.go b/internal/websocket/x/websocket_test.go new file mode 100644 index 00000000..708bc8cb --- /dev/null +++ b/internal/websocket/x/websocket_test.go @@ -0,0 +1 @@ +package websocket diff --git a/service/jam/service.go b/service/jam/service.go index c28f10d1..5071b9ca 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -2,11 +2,9 @@ package jam import ( "context" - "encoding/json" "errors" "log" "net/http" - "sync" "github.com/go-chi/chi/v5" "github.com/gorilla/websocket" @@ -20,34 +18,6 @@ import ( w2 "github.com/rog-golang-buddies/rmx/internal/websocket/x" ) -type Pool struct { - mu sync.Mutex - m map[w2.Conn]bool -} - -func (p *Pool) BroadcastString(s string) error { - p.mu.Lock() - defer p.mu.Unlock() - - for c := range p.m { - if err := c.WriteString(s); err != nil { - return err - } - } - - return nil -} - -func (p *Pool) remove(c w2.Conn) error { - p.mu.Lock() - defer p.mu.Unlock() - if err := c.Close(); err != nil { - return err - } - delete(p.m, c) - return nil -} - func (s *Service) routes() { s.m.Route("/api/v1/jam", func(r chi.Router) { r.Get("/", s.handleListRooms()) @@ -55,114 +25,54 @@ func (s *Service) routes() { r.Get("/{uuid}", s.handleGetRoom()) }) - s.m.Route("/ws", func(r chi.Router) { - - pool := Pool{m: make(map[w2.Conn]bool)} - - r.Route("/echo", func(r chi.Router) { - r.Get("/", func(w http.ResponseWriter, r *http.Request) { - conn, err := w2.UpgradeHTTP(w, r) - if err != nil { - s.respond(w, r, err, http.StatusUpgradeRequired) - return - } - - pool.m[conn] = true - - defer pool.remove(conn) - for { - msg, err := conn.ReadString() - if err != nil { - return - } - // s.log("connection added") - if err := pool.BroadcastString(msg); err != nil { - return - } - } - }) - }) + // create a single Pool + pool := &w2.Pool{Capacity: 2} - r.Route("/jam", func(r chi.Router) { - r = r.With(s.connectionPool(nil), s.upgradeHTTP(1024, 1024)) - r.Get("/{uuid}", s.handleP2PComms()) - }) + s.m.Route("/ws/jam", func(r chi.Router) { + r.Get("/{uuid}", s.handleP2PComms(pool)) }) } -func (s *Service) handleP2PComms() http.HandlerFunc { - // FIXME we will change this as I know this hasn't been - // was just my way of getting things working, not yet - // full agreement with this. - type response[T any] struct { - Typ internal.MsgTyp `json:"type"` - Payload T `json:"payload"` - } - - type join struct { - ID suid.SUID `json:"id"` - SessionID suid.SUID `json:"sessionId"` - } - - type leave struct { - ID suid.SUID `json:"id"` - SessionID suid.SUID `json:"sessionId"` - Error any `json:"err"` - } - +func (s *Service) handleP2PComms(pool *w2.Pool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - c := r.Context().Value(internal.UpgradeKey).(*ws.Conn) - - defer func() { - // FIXME send error when Leaving session pool - c.SendMessage(response[leave]{ - Typ: internal.Leave, - Payload: leave{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, - }) - - c.Close() - }() - - if err := c.SendMessage(response[join]{ - Typ: internal.Join, - Payload: join{ID: c.ID.ShortUUID(), SessionID: c.Pool().ID.ShortUUID()}, - }); err != nil { - s.log(err) + if pool.IsCap() { + s.respond(w, r, "error: pool has reached capacity", http.StatusUpgradeRequired) + return + } + + conn, err := w2.UpgradeHTTP(w, r) + if err != nil { + s.respond(w, r, err, http.StatusUpgradeRequired) return } - // TODO could the API be adjusted such that - // this for-loop only needs to read and - // never touch the code for writing + pool.Append(conn) + defer pool.Remove(conn) for { - var msg response[json.RawMessage] - if err := c.ReadJSON(&msg); err != nil { - s.log(err) + msg, err := conn.ReadString() + if err != nil { return } - // TODO here the message will be passed off to a different handler - // via a go routine* - if err := c.SendMessage(response[int]{Typ: internal.Message, Payload: 10}); err != nil { - s.log(err) - return - } + pool.Broadcast(msg) } } } func (s *Service) handleCreateRoom() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.c.NewPool(4) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } + // uid, err := s.c.NewPool(4) + // if err != nil { + // s.respond(w, r, err, http.StatusInternalServerError) + // return + // } - v := &session{ID: suid.FromUUID(uid)} + // v := &session{ID: suid.FromUUID(uid)} - s.respond(w, r, v, http.StatusOK) + // s.respond(w, r, v, http.StatusOK) + + s.respond(w, r, nil, http.StatusNotImplemented) } } diff --git a/service/jam/service_test.go b/service/jam/service_test.go index aa9a8fd8..e7932df4 100644 --- a/service/jam/service_test.go +++ b/service/jam/service_test.go @@ -2,17 +2,19 @@ package jam import ( "context" + "net/http" "net/http/httptest" "strings" "testing" "github.com/go-chi/chi/v5" "github.com/hyphengolang/prelude/testing/is" + "github.com/rog-golang-buddies/rmx/internal/suid" w2 "github.com/rog-golang-buddies/rmx/internal/websocket/x" // "github.com/rog-golang-buddies/rmx/internal/websocket" ) -func TestMultipleClients(t *testing.T) { +func TestService(t *testing.T) { t.Parallel() is := is.New(t) @@ -21,36 +23,35 @@ func TestMultipleClients(t *testing.T) { t.Cleanup(func() { srv.Close() }) - t.Run(`connect to echo server`, func(t *testing.T) { - c1, err := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/echo") - is.NoErr(err) // dial error + t.Run(`create a new jam room`, func(t *testing.T) { + r, _ := srv.Client().Get(srv.URL + "/api/v1/jam") + is.Equal(r.StatusCode, http.StatusOK) // successfully created a new room + }) - err = c1.WriteString("Hello, World!") - is.NoErr(err) // write string to server + t.Run(`connect users to room websocket`, func(t *testing.T) { - msg, err := c1.ReadString() - is.NoErr(err) // read string from server + jamId := suid.NewSUID() - is.Equal(msg, "Hello, World!") // server message == client message + c1, err := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/jam/"+jamId.String()) + is.NoErr(err) // connect client 1 + c2, err := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/jam/"+jamId.String()) + is.NoErr(err) // connect client 2 - c1.Close() - }) + t.Cleanup(func() { + c1.Close() + c2.Close() + }) - t.Run(`communicate between two connections`, func(t *testing.T) { - c2, _ := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/echo") - c3, _ := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/echo") + _, err = w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/jam/"+jamId.String()) + is.True(err != nil) // cannot connect client 2 - err := c2.WriteString("Hello, World!") + err = c1.WriteString("Hello, World!") is.NoErr(err) // write string to pool - // time.Sleep(time.Second * 1) - - msg, err := c3.ReadString() + msg, err := c2.ReadString() is.NoErr(err) // read message sent by c1 is.Equal(msg, "Hello, World!") - c2.Close() - c3.Close() }) } From 3ca5f3453d4e75dcba31364261a8017710dadcf3 Mon Sep 17 00:00:00 2001 From: aboublef Date: Sat, 29 Oct 2022 23:18:55 +0100 Subject: [PATCH 212/246] experimental websocket api --- go.mod | 1 + go.sum | 2 + internal/websocket/x/client.go | 14 +-- service/jam/service.go | 151 +++++++++++++-------------------- service/jam/service_test.go | 32 +++++-- 5 files changed, 95 insertions(+), 105 deletions(-) diff --git a/go.mod b/go.mod index e99326fb..81c80a29 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/goccy/go-json v0.9.11 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect + github.com/jackc/puddle/v2 v2.0.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect diff --git a/go.sum b/go.sum index 2e11092a..dee44a82 100644 --- a/go.sum +++ b/go.sum @@ -59,6 +59,8 @@ github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHF github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgx/v5 v5.0.3 h1:4flM5ecR/555F0EcnjdaZa6MhBU+nr0QbZIo5vaKjuM= github.com/jackc/pgx/v5 v5.0.3/go.mod h1:JBbvW3Hdw77jKl9uJrEDATUZIFM2VFPzRq4RWIhkF4o= +github.com/jackc/puddle/v2 v2.0.0 h1:Kwk/AlLigcnZsDssc3Zun1dk1tAtQNPaBBxBHWn0Mjc= +github.com/jackc/puddle/v2 v2.0.0/go.mod h1:itE7ZJY8xnoo0JqJEpSMprN0f+NQkMCuEV/N9j8h0oc= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= diff --git a/internal/websocket/x/client.go b/internal/websocket/x/client.go index 0b213d46..e5921242 100644 --- a/internal/websocket/x/client.go +++ b/internal/websocket/x/client.go @@ -2,8 +2,16 @@ package websocket import ( "sync" + + "github.com/rog-golang-buddies/rmx/internal/suid" ) +type Multiplexer struct { + ps map[suid.SUID]*Pool +} + +func (mux *Multiplexer) Append(sid suid.SUID, pool Pool) {} + type Pool struct { Capacity uint @@ -11,14 +19,9 @@ type Pool struct { cs sync.Map } -func NewPool(cap uint) *Pool { - return &Pool{Capacity: cap} -} - func (p *Pool) Append(conn Conn) { p.cs.Store(conn, struct{}{}) p.seq++ - } func (p *Pool) IsCap() bool { return p.seq >= p.Capacity } @@ -29,6 +32,7 @@ func (p *Pool) Remove(conn Conn) error { return conn.Close() } +// General broadcast method that can be used with any message type func (p *Pool) Broadcast(msg any) { var f func(key, value any) bool diff --git a/service/jam/service.go b/service/jam/service.go index 5071b9ca..3864d500 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -5,53 +5,75 @@ import ( "errors" "log" "net/http" + "sync" "github.com/go-chi/chi/v5" - "github.com/gorilla/websocket" h "github.com/hyphengolang/prelude/http" - "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/suid" ws "github.com/rog-golang-buddies/rmx/internal/websocket" - w2 "github.com/rog-golang-buddies/rmx/internal/websocket/x" + websocket "github.com/rog-golang-buddies/rmx/internal/websocket/x" ) +type Service struct { + m chi.Router + c *ws.Client + + log func(s ...any) + logf func(string, ...any) + + created func(http.ResponseWriter, *http.Request, string) + respond func(http.ResponseWriter, *http.Request, any, int) + decode func(http.ResponseWriter, *http.Request, interface{}) error +} + func (s *Service) routes() { + // key=suid.SUID <&> value=*websocket.Pool + var mux sync.Map + s.m.Route("/api/v1/jam", func(r chi.Router) { r.Get("/", s.handleListRooms()) - r.Post("/", s.handleCreateRoom()) + r.Post("/", s.handleCreateRoom(&mux)) r.Get("/{uuid}", s.handleGetRoom()) }) - // create a single Pool - pool := &w2.Pool{Capacity: 2} - s.m.Route("/ws/jam", func(r chi.Router) { - r.Get("/{uuid}", s.handleP2PComms(pool)) + r.Get("/{uuid}", s.handleP2PComms(&mux)) }) } -func (s *Service) handleP2PComms(pool *w2.Pool) http.HandlerFunc { +func (s *Service) handleP2PComms(mux *sync.Map) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - if pool.IsCap() { - s.respond(w, r, "error: pool has reached capacity", http.StatusUpgradeRequired) + // decode uuid from + sid, err := s.parseUUID(w, r) + if err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } + + // NOTE this is avoidable, maybe(?) + value, ok := mux.Load(sid.ShortUUID()) + if !ok { + s.respond(w, r, "not found", http.StatusNotFound) return } - conn, err := w2.UpgradeHTTP(w, r) + // NOTE this is avoidable, maybe(?) + pool := (value).(*websocket.Pool) + conn, close, err := s.upgradeHTTP(w, r, pool) if err != nil { s.respond(w, r, err, http.StatusUpgradeRequired) return } - pool.Append(conn) - defer pool.Remove(conn) + defer close() for { msg, err := conn.ReadString() if err != nil { + conn.WriteString(err.Error()) return } @@ -60,19 +82,23 @@ func (s *Service) handleP2PComms(pool *w2.Pool) http.HandlerFunc { } } -func (s *Service) handleCreateRoom() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // uid, err := s.c.NewPool(4) - // if err != nil { - // s.respond(w, r, err, http.StatusInternalServerError) - // return - // } +func (s *Service) handleCreateRoom(mux *sync.Map) http.HandlerFunc { + type payload struct { + Capacity uint `json:"capacity"` + } - // v := &session{ID: suid.FromUUID(uid)} + return func(w http.ResponseWriter, r *http.Request) { + var pl payload + if err := s.decode(w, r, &pl); err != nil { + s.respond(w, r, err, http.StatusBadRequest) + return + } - // s.respond(w, r, v, http.StatusOK) + // NOTE add to Mux/Router/Client whatever it will be called + sid := suid.NewSUID() + mux.Store(sid, &websocket.Pool{Capacity: pl.Capacity}) - s.respond(w, r, nil, http.StatusNotImplemented) + s.created(w, r, sid.String()) } } @@ -124,65 +150,17 @@ func (s *Service) handleListRooms() http.HandlerFunc { } } -// needs to be moved into websocket package as its middleware -func (s *Service) connectionPool(p *ws.Pool) func(f http.Handler) http.Handler { - return func(f http.Handler) http.Handler { - var fn func(w http.ResponseWriter, r *http.Request) - if p != nil { - fn = func(w http.ResponseWriter, r *http.Request) { - f.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), internal.RoomKey, p))) - } - } else { - fn = func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r) - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return - } - - p, err := s.c.Get(uid) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - r = r.WithContext(context.WithValue(r.Context(), internal.RoomKey, p)) - f.ServeHTTP(w, r) - } - } - - return http.HandlerFunc(fn) +func (s *Service) upgradeHTTP(w http.ResponseWriter, r *http.Request, pool *websocket.Pool) (conn websocket.Conn, close func(), err error) { + if pool.IsCap() { + return nil, nil, errors.New("error: pool has reached capacity") } -} - -// needs to be moved into websocket package as its middleware -func (s *Service) upgradeHTTP(readBuf, writeBuf int) func(f http.Handler) http.Handler { - return func(f http.Handler) http.Handler { - u := &websocket.Upgrader{ - ReadBufferSize: readBuf, - WriteBufferSize: writeBuf, - CheckOrigin: func(r *http.Request) bool { return true }, - } - - fn := func(w http.ResponseWriter, r *http.Request) { - p, _ := r.Context().Value(internal.RoomKey).(*ws.Pool) - if p.Size() == p.MaxConn { - s.respond(w, r, ws.ErrMaxConn, http.StatusUnauthorized) - return - } - - c, err := p.NewConn(w, r, u) - if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) - return - } - r = r.WithContext(context.WithValue(r.Context(), internal.UpgradeKey, c)) - f.ServeHTTP(w, r) - } - - return http.HandlerFunc(fn) + if conn, err = websocket.UpgradeHTTP(w, r); err != nil { + return nil, nil, err } + + pool.Append(conn) + return conn, func() { pool.Remove(conn) }, nil } var ( @@ -191,23 +169,12 @@ var ( ErrSessionExists = errors.New("api: session already exists") ) -type Service struct { - m chi.Router - c *ws.Client - - log func(s ...any) - logf func(string, ...any) - - respond func(http.ResponseWriter, *http.Request, any, int) - decode func(http.ResponseWriter, *http.Request, interface{}) error -} - func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } func NewService(ctx context.Context, r chi.Router) *Service { s := &Service{ r, - ws.DefaultClient, log.Print, log.Printf, h.Respond, h.Decode, + ws.DefaultClient, log.Print, log.Printf, h.Created, h.Respond, h.Decode, } s.routes() return s diff --git a/service/jam/service_test.go b/service/jam/service_test.go index e7932df4..28483cde 100644 --- a/service/jam/service_test.go +++ b/service/jam/service_test.go @@ -9,11 +9,14 @@ import ( "github.com/go-chi/chi/v5" "github.com/hyphengolang/prelude/testing/is" - "github.com/rog-golang-buddies/rmx/internal/suid" w2 "github.com/rog-golang-buddies/rmx/internal/websocket/x" // "github.com/rog-golang-buddies/rmx/internal/websocket" ) +var resource = func(s string) string { + return s[strings.LastIndex(s, "/")+1:] +} + func TestService(t *testing.T) { t.Parallel() is := is.New(t) @@ -23,18 +26,31 @@ func TestService(t *testing.T) { t.Cleanup(func() { srv.Close() }) - t.Run(`create a new jam room`, func(t *testing.T) { + t.Run(`list all jam sessions`, func(t *testing.T) { r, _ := srv.Client().Get(srv.URL + "/api/v1/jam") is.Equal(r.StatusCode, http.StatusOK) // successfully created a new room }) - t.Run(`connect users to room websocket`, func(t *testing.T) { + var firstPool string + t.Run(`create a new room`, func(t *testing.T) { + payload := ` +{ + "capacity":2 +}` + res, err := srv.Client().Post(srv.URL+"/api/v1/jam", "application/json", strings.NewReader(payload)) + is.NoErr(err) // create a new pool + is.Equal(res.StatusCode, http.StatusCreated) // created a new resource - jamId := suid.NewSUID() + loc, err := res.Location() + is.NoErr(err) // retrieve location - c1, err := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/jam/"+jamId.String()) + firstPool = resource(loc.Path) + }) + + t.Run(`connect users to room websocket`, func(t *testing.T) { + c1, err := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/jam/"+firstPool) is.NoErr(err) // connect client 1 - c2, err := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/jam/"+jamId.String()) + c2, err := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/jam/"+firstPool) is.NoErr(err) // connect client 2 t.Cleanup(func() { @@ -42,8 +58,8 @@ func TestService(t *testing.T) { c2.Close() }) - _, err = w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/jam/"+jamId.String()) - is.True(err != nil) // cannot connect client 2 + _, err = w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/jam/"+firstPool) + is.True(err != nil) // cannot connect client 3 err = c1.WriteString("Hello, World!") is.NoErr(err) // write string to pool From f38f4518fe53a7ceaca4998e02b480a92a884caa Mon Sep 17 00:00:00 2001 From: aboublef Date: Sat, 29 Oct 2022 23:19:30 +0100 Subject: [PATCH 213/246] store updates --- store/store.go | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/store/store.go b/store/store.go index 233e5b34..ec27bd29 100644 --- a/store/store.go +++ b/store/store.go @@ -3,33 +3,39 @@ package store import ( "context" + "github.com/jackc/pgx/v5/pgxpool" "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/store/sql/user" ) type Store struct { - ctx context.Context - tc internal.TokenClient - ur internal.UserRepo + ur *user.Repo } -func (s *Store) UserRepo() internal.UserRepo { +func (s *Store) UserRepo() *user.Repo { + if s.ur == nil { + panic("user repo must not be nil") + } return s.ur } func (s *Store) TokenClient() internal.TokenClient { + if s.tc == nil { + panic("token client must not be nil") + } return s.tc } -// FIXME this needs to be fleshed out properly -func New(ctx context.Context, connString string) *Store { - s := &Store{ctx: ctx} - return s -} +func New(ctx context.Context, connString string) (*Store, error) { + pool, err := pgxpool.New(ctx, connString) + if err != nil { + return nil, err + } -func (s *Store) Context() context.Context { - if s.ctx != nil { - return s.ctx + s := &Store{ + ur: user.NewRepo(ctx, pool), } - return context.Background() + + return s, nil } From 8c136c8541269be63509a11d46154517c7d370b6 Mon Sep 17 00:00:00 2001 From: aboublef Date: Sat, 5 Nov 2022 10:51:58 +0000 Subject: [PATCH 214/246] store/sql/user -> store/user --- store/sql/schema/migration.sql | 11 ----- store/sql/schema/user_query.sql | 36 -------------- store/sql/schema/user_schema.sql | 10 ---- store/sql/user/psql.sql | 50 -------------------- store/store.go | 2 +- store/{sql => }/user/mysql.sql | 0 store/{sql => }/user/repo.go | 78 +++++++++++++++---------------- store/{sql => }/user/repo_test.go | 0 8 files changed, 40 insertions(+), 147 deletions(-) delete mode 100644 store/sql/schema/migration.sql delete mode 100644 store/sql/schema/user_query.sql delete mode 100644 store/sql/schema/user_schema.sql delete mode 100644 store/sql/user/psql.sql rename store/{sql => }/user/mysql.sql (100%) rename store/{sql => }/user/repo.go (70%) rename store/{sql => }/user/repo_test.go (100%) diff --git a/store/sql/schema/migration.sql b/store/sql/schema/migration.sql deleted file mode 100644 index 062cb892..00000000 --- a/store/sql/schema/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ --- +migrate UP -CREATE TABLE users ( - id text NOT NULL PRIMARY KEY, - username text NOT NULL, - email text NOT NULL, - password text NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - deleted_at TIMESTAMP NULL DEFAULT NULL, - UNIQUE (email) -); diff --git a/store/sql/schema/user_query.sql b/store/sql/schema/user_query.sql deleted file mode 100644 index 8301d4af..00000000 --- a/store/sql/schema/user_query.sql +++ /dev/null @@ -1,36 +0,0 @@ --- name: GetUserByID :one -SELECT * -FROM users -WHERE id = ? -LIMIT 1; - --- name: GetUserByEmail :one -SELECT * -FROM users -WHERE email = ? -LIMIT 1; - --- name: ListUsers :many -SELECT * -FROM users -ORDER BY id; - --- name: CreateUser :execresult -INSERT INTO users ( - username, - email, - password, - created_at, - updated_at, - deleted_at - ) -VALUES (?, ?, ?, ?, ?, ?); - --- name: UpdateUser :execresult -UPDATE users - SET username = ? - WHERE id = ?; - --- name: DeleteUser :exec -DELETE FROM users -WHERE id = ?; diff --git a/store/sql/schema/user_schema.sql b/store/sql/schema/user_schema.sql deleted file mode 100644 index 4e80e1f5..00000000 --- a/store/sql/schema/user_schema.sql +++ /dev/null @@ -1,10 +0,0 @@ -CREATE TABLE users ( - id text NOT NULL PRIMARY KEY, - username text NOT NULL, - email text NOT NULL, - password text NOT NULL, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - deleted_at TIMESTAMP NULL DEFAULT NULL, - UNIQUE (email) -); diff --git a/store/sql/user/psql.sql b/store/sql/user/psql.sql deleted file mode 100644 index e7ee1350..00000000 --- a/store/sql/user/psql.sql +++ /dev/null @@ -1,50 +0,0 @@ -CREATE TEMP TABLE IF NOT EXISTS "users" ( - id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (), - username text UNIQUE NOT NULL CHECK (username <> ''), - email citext UNIQUE NOT NULL CHECK (email ~ '^[a-zA-Z0-9.!#$%&’*+/=?^_\x60{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$'), - PASSWORD citext NOT NULL CHECK (PASSWORD <> ''), - created_at timestamp DEFAULT now() -); - --- name: SelectByID :one -SELECT - * -FROM - users -WHERE - id = $1 -LIMIT 1; - --- name: SelectByEmail :one -SELECT - * -FROM - users -WHERE - email = $1 -LIMIT 1; - --- name: SelectMany :many -SELECT - * -FROM - users -ORDER BY - id; - --- name: CreateUser :execresult -INSERT INTO users (username, email, PASSWORD, created_at) - VALUES ($1, $2, $3, $4); - --- name: UpdateUser :execresult -UPDATE - users -SET - username = $1 -WHERE - id = $2; - --- name: DeleteUser :exec -DELETE FROM users -WHERE id = $1; - diff --git a/store/store.go b/store/store.go index ec27bd29..77bb3071 100644 --- a/store/store.go +++ b/store/store.go @@ -5,7 +5,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/store/sql/user" + "github.com/rog-golang-buddies/rmx/store/user" ) type Store struct { diff --git a/store/sql/user/mysql.sql b/store/user/mysql.sql similarity index 100% rename from store/sql/user/mysql.sql rename to store/user/mysql.sql diff --git a/store/sql/user/repo.go b/store/user/repo.go similarity index 70% rename from store/sql/user/repo.go rename to store/user/repo.go index ed4d918d..2f86d74c 100644 --- a/store/sql/user/repo.go +++ b/store/user/repo.go @@ -59,16 +59,16 @@ func NewRepo(ctx context.Context, conn *pgxpool.Pool) *Repo { return &Repo{ctx, conn} } -func (s *Repo) Context() context.Context { - if s.ctx != nil { - return s.ctx +func (r *Repo) Context() context.Context { + if r.ctx != nil { + return r.ctx } return context.Background() } -func (s *Repo) Close() { s.c.Close() } +func (r *Repo) Close() { r.c.Close() } -func (s *Repo) Insert(ctx context.Context, u *internal.User) error { +func (r *Repo) Insert(ctx context.Context, u *internal.User) error { args := pgx.NamedArgs{ "id": u.ID, "email": u.Email, @@ -76,16 +76,16 @@ func (s *Repo) Insert(ctx context.Context, u *internal.User) error { "password": u.Password, } - return psql.Exec(s.c, qryInsert, args) + return psql.Exec(r.c, qryInsert, args) } -func (s *Repo) SelectMany(ctx context.Context) ([]internal.User, error) { - return psql.Query(s.c, qrySelectMany, func(r pgx.Rows, u *internal.User) error { +func (r *Repo) SelectMany(ctx context.Context) ([]internal.User, error) { + return psql.Query(r.c, qrySelectMany, func(r pgx.Rows, u *internal.User) error { return r.Scan(&u.ID, &u.Email, &u.Username, &u.Password) }) } -func (s *Repo) Select(ctx context.Context, key any) (*internal.User, error) { +func (r *Repo) Select(ctx context.Context, key any) (*internal.User, error) { var qry string switch key.(type) { case suid.UUID: @@ -98,10 +98,10 @@ func (s *Repo) Select(ctx context.Context, key any) (*internal.User, error) { return nil, internal.ErrInvalidType } var u internal.User - return &u, psql.QueryRow(s.c, qry, func(r pgx.Row) error { return r.Scan(&u.ID, &u.Username, &u.Email, &u.Password) }, key) + return &u, psql.QueryRow(r.c, qry, func(r pgx.Row) error { return r.Scan(&u.ID, &u.Username, &u.Email, &u.Password) }, key) } -func (s *Repo) Delete(ctx context.Context, key any) error { +func (r *Repo) Delete(ctx context.Context, key any) error { var qry string switch key.(type) { case suid.UUID: @@ -113,23 +113,23 @@ func (s *Repo) Delete(ctx context.Context, key any) error { default: return internal.ErrInvalidType } - return psql.Exec(s.c, qry, key) + return psql.Exec(r.c, qry, key) } // NOTE in-memory implementation not required anymore -var DefaultRepo = &store{ +var DefaultRepo = &repo{ miu: make(map[suid.UUID]*User), mei: make(map[string]*User), log: log.Println, logf: log.Printf, } -func (s *store) Close() {} +func (r *repo) Close() {} -func (s *store) Delete(ctx context.Context, key any) error { return nil } +func (r *repo) Delete(ctx context.Context, key any) error { return nil } -type store struct { +type repo struct { mu sync.Mutex miu map[suid.UUID]*User mei map[string]*User @@ -139,15 +139,15 @@ type store struct { } // Remove implements internal.UserRepo -func (s *store) Remove(ctx context.Context, key any) error { +func (r *repo) Remove(ctx context.Context, key any) error { panic("unimplemented") } -func (s *store) Insert(ctx context.Context, iu *internal.User) error { - s.mu.Lock() - defer s.mu.Unlock() +func (r *repo) Insert(ctx context.Context, iu *internal.User) error { + r.mu.Lock() + defer r.mu.Unlock() - if _, found := s.mei[iu.Email.String()]; found { + if _, found := r.mei[iu.Email.String()]; found { return internal.ErrAlreadyExists } @@ -158,33 +158,33 @@ func (s *store) Insert(ctx context.Context, iu *internal.User) error { Password: iu.Password, CreatedAt: time.Now(), } - s.mei[iu.Email.String()], s.miu[iu.ID] = u, u + r.mei[iu.Email.String()], r.miu[iu.ID] = u, u return nil } -func (s *store) SelectMany(ctx context.Context) ([]internal.User, error) { +func (r *repo) SelectMany(ctx context.Context) ([]internal.User, error) { panic("not implemented") } -func (s *store) Select(ctx context.Context, key any) (*internal.User, error) { +func (r *repo) Select(ctx context.Context, key any) (*internal.User, error) { switch key := key.(type) { case suid.UUID: - return s.selectUUID(key) + return r.selectUUID(key) case email.Email: - return s.selectEmail(key) + return r.selectEmail(key) case string: - return s.selectUsername(key) + return r.selectUsername(key) default: return nil, internal.ErrInvalidType } } -func (s *store) selectUUID(uid suid.UUID) (*internal.User, error) { - s.mu.Lock() - defer s.mu.Unlock() +func (r *repo) selectUUID(uid suid.UUID) (*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() - if u, ok := s.miu[uid]; ok { + if u, ok := r.miu[uid]; ok { return &internal.User{ ID: u.ID, Username: u.Username, @@ -196,11 +196,11 @@ func (s *store) selectUUID(uid suid.UUID) (*internal.User, error) { return nil, internal.ErrNotFound } -func (s *store) selectUsername(username string) (*internal.User, error) { - s.mu.Lock() - defer s.mu.Unlock() +func (r *repo) selectUsername(username string) (*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() - for _, u := range s.mei { + for _, u := range r.mei { if u.Username == username { return &internal.User{ ID: u.ID, @@ -214,11 +214,11 @@ func (s *store) selectUsername(username string) (*internal.User, error) { return nil, internal.ErrNotFound } -func (s *store) selectEmail(email email.Email) (*internal.User, error) { - s.mu.Lock() - defer s.mu.Unlock() +func (r *repo) selectEmail(email email.Email) (*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() - if u, ok := s.mei[email.String()]; ok { + if u, ok := r.mei[email.String()]; ok { return &internal.User{ ID: u.ID, Username: u.Username, diff --git a/store/sql/user/repo_test.go b/store/user/repo_test.go similarity index 100% rename from store/sql/user/repo_test.go rename to store/user/repo_test.go From e1dfc82f09604f58b3dc8c6065a61bd268fea7c6 Mon Sep 17 00:00:00 2001 From: aboublef Date: Sat, 5 Nov 2022 12:32:39 +0000 Subject: [PATCH 215/246] websockets create and join --- service/jam/service.go | 223 +++++++++++++++++++++--------------- service/jam/service_test.go | 105 ++++++++++++----- service/jam/websocket.go | 212 ++++++++++++++++++++++++++++++++++ 3 files changed, 419 insertions(+), 121 deletions(-) create mode 100644 service/jam/websocket.go diff --git a/service/jam/service.go b/service/jam/service.go index 3864d500..f61b75e3 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -11,12 +11,27 @@ import ( h "github.com/hyphengolang/prelude/http" - "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/suid" ws "github.com/rog-golang-buddies/rmx/internal/websocket" - websocket "github.com/rog-golang-buddies/rmx/internal/websocket/x" ) +// Jam Service Endpoints +// +// Create a new jam session. +// +// POST /api/v1/jam +// +// List all jam sessions metadata. +// +// GET /api/v1/jam +// +// Get a jam sessions metadata. +// +// GET /api/v1/jam/{uuid} +// +// Connect to jam session. +// +// GET /ws/jam/{uuid} type Service struct { m chi.Router c *ws.Client @@ -29,23 +44,58 @@ type Service struct { decode func(http.ResponseWriter, *http.Request, interface{}) error } +type muxEntry struct { + sid suid.UUID + pool *Pool +} + +func (e muxEntry) String() string { return e.sid.ShortUUID().String() } + +type mux struct { + mu sync.Mutex + mp map[suid.UUID]muxEntry +} + +func (mux *mux) Store(e muxEntry) { + mux.mu.Lock() + { + mux.mp[e.sid] = e + } + mux.mu.Unlock() +} + +func (mux *mux) Load(sid suid.UUID) (pool *Pool, err error) { + mux.mu.Lock() + e, ok := mux.mp[sid] + mux.mu.Unlock() + + if !ok { + return nil, errors.New("pool not found") + } + + return e.pool, nil +} + func (s *Service) routes() { - // key=suid.SUID <&> value=*websocket.Pool - var mux sync.Map + // NOTE this map is temporary + // map[suid.SUID]*websocket.Pool + var mux = &mux{ + mp: make(map[suid.UUID]muxEntry), + } s.m.Route("/api/v1/jam", func(r chi.Router) { - r.Get("/", s.handleListRooms()) - r.Post("/", s.handleCreateRoom(&mux)) - r.Get("/{uuid}", s.handleGetRoom()) + // r.Get("/", s.handleListRooms()) + r.Post("/", s.handleCreateJamRoom(mux)) + // r.Get("/{uuid}", s.handleGetRoomData(mux)) }) s.m.Route("/ws/jam", func(r chi.Router) { - r.Get("/{uuid}", s.handleP2PComms(&mux)) + r.Get("/{uuid}", s.handleP2PComms(mux)) }) } -func (s *Service) handleP2PComms(mux *sync.Map) http.HandlerFunc { +func (s *Service) handleP2PComms(mux *mux) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // decode uuid from sid, err := s.parseUUID(w, r) @@ -54,35 +104,23 @@ func (s *Service) handleP2PComms(mux *sync.Map) http.HandlerFunc { return } - // NOTE this is avoidable, maybe(?) - value, ok := mux.Load(sid.ShortUUID()) - if !ok { - s.respond(w, r, "not found", http.StatusNotFound) + pool, err := mux.Load(sid) + if err != nil { + s.respond(w, r, err, http.StatusNotFound) return } - // NOTE this is avoidable, maybe(?) - pool := (value).(*websocket.Pool) - conn, close, err := s.upgradeHTTP(w, r, pool) + rwc, err := UpgradeHTTP(w, r) if err != nil { s.respond(w, r, err, http.StatusUpgradeRequired) return } - defer close() - for { - msg, err := conn.ReadString() - if err != nil { - conn.WriteString(err.Error()) - return - } - - pool.Broadcast(msg) - } + pool.ListenAndServe(rwc) } } -func (s *Service) handleCreateRoom(mux *sync.Map) http.HandlerFunc { +func (s *Service) handleCreateJamRoom(mux *mux) http.HandlerFunc { type payload struct { Capacity uint `json:"capacity"` } @@ -94,74 +132,77 @@ func (s *Service) handleCreateRoom(mux *sync.Map) http.HandlerFunc { return } - // NOTE add to Mux/Router/Client whatever it will be called - sid := suid.NewSUID() - mux.Store(sid, &websocket.Pool{Capacity: pl.Capacity}) - - s.created(w, r, sid.String()) - } -} - -func (s *Service) handleGetRoom() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - uid, err := s.parseUUID(w, r) - if err != nil { - s.respond(w, r, err, http.StatusBadRequest) - return + pool := &Pool{ + Capacity: pl.Capacity, } - // FIXME possible rename - // method as `Get` is nondescriptive - p, err := s.c.Get(uid) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - v := &session{ - ID: p.ID.ShortUUID(), - Users: fp.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), - } - - s.respond(w, r, v, http.StatusOK) - } -} - -func (s *Service) handleListRooms() http.HandlerFunc { - type response struct { - Sessions []session `json:"sessions"` - } - - return func(w http.ResponseWriter, r *http.Request) { - v := &response{ - Sessions: fp.FMap(s.c.List(), func(p *ws.Pool) session { - return session{ - ID: p.ID.ShortUUID(), - Users: fp.FMap( - p.Keys(), - func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }, - ), - UserCount: p.Size(), - } - }), - } + e := muxEntry{suid.NewUUID(), pool} - s.respond(w, r, v, http.StatusOK) + mux.Store(e) + s.created(w, r, e.String()) } } -func (s *Service) upgradeHTTP(w http.ResponseWriter, r *http.Request, pool *websocket.Pool) (conn websocket.Conn, close func(), err error) { - if pool.IsCap() { - return nil, nil, errors.New("error: pool has reached capacity") - } - - if conn, err = websocket.UpgradeHTTP(w, r); err != nil { - return nil, nil, err - } - - pool.Append(conn) - return conn, func() { pool.Remove(conn) }, nil -} +// func (s *Service) handleGetRoomData(mux *mux) http.HandlerFunc { +// return func(w http.ResponseWriter, r *http.Request) { +// uid, err := s.parseUUID(w, r) +// if err != nil { +// s.respond(w, r, err, http.StatusBadRequest) +// return +// } + +// // FIXME possible rename +// // method as `Get` is nondescriptive +// p, err := s.c.Get(uid) +// if err != nil { +// s.respond(w, r, err, http.StatusNotFound) +// return +// } + +// v := &Session{ +// ID: p.ID.ShortUUID(), +// Users: fp.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), +// } + +// s.respond(w, r, v, http.StatusOK) +// } +// } + +// func (s *Service) handleListRooms() http.HandlerFunc { +// type response struct { +// Sessions []Session `json:"sessions"` +// } + +// return func(w http.ResponseWriter, r *http.Request) { +// v := &response{ +// Sessions: fp.FMap(s.c.List(), func(p *ws.Pool) Session { +// return Session{ +// ID: p.ID.ShortUUID(), +// Users: fp.FMap( +// p.Keys(), +// func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }, +// ), +// UserCount: p.Size(), +// } +// }), +// } + +// s.respond(w, r, v, http.StatusOK) +// } +// } + +// func (s *Service) upgradeHTTP(w http.ResponseWriter, r *http.Request, pool *websocket.Pool) (conn websocket.Conn, close func(), err error) { +// if pool.IsCap() { +// return nil, nil, errors.New("error: pool has reached capacity") +// } + +// if conn, err = websocket.UpgradeHTTP(w, r); err != nil { +// return nil, nil, err +// } + +// pool.Append(conn) +// return conn, func() { pool.Remove(conn) }, nil +// } var ( ErrNoCookie = errors.New("api: cookie not found") @@ -184,13 +225,13 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, return suid.ParseString(chi.URLParam(r, "uuid")) } -type jam struct { +type Jam struct { Name string `json:"name"` BPM int `json:"bpm"` ws.Pool } -type session struct { +type Session struct { ID suid.SUID `json:"id"` Name string `json:"name,omitempty"` Users []suid.SUID `json:"users,omitempty"` diff --git a/service/jam/service_test.go b/service/jam/service_test.go index 28483cde..99ffb13b 100644 --- a/service/jam/service_test.go +++ b/service/jam/service_test.go @@ -8,8 +8,8 @@ import ( "testing" "github.com/go-chi/chi/v5" + "github.com/gorilla/websocket" "github.com/hyphengolang/prelude/testing/is" - w2 "github.com/rog-golang-buddies/rmx/internal/websocket/x" // "github.com/rog-golang-buddies/rmx/internal/websocket" ) @@ -17,6 +17,10 @@ var resource = func(s string) string { return s[strings.LastIndex(s, "/")+1:] } +var stripPrefix = func(s string) string { + return "ws" + strings.TrimPrefix(s, "http") +} + func TestService(t *testing.T) { t.Parallel() is := is.New(t) @@ -26,19 +30,14 @@ func TestService(t *testing.T) { t.Cleanup(func() { srv.Close() }) - t.Run(`list all jam sessions`, func(t *testing.T) { - r, _ := srv.Client().Get(srv.URL + "/api/v1/jam") - is.Equal(r.StatusCode, http.StatusOK) // successfully created a new room - }) - var firstPool string - t.Run(`create a new room`, func(t *testing.T) { + t.Run(`create a new Room`, func(t *testing.T) { payload := ` -{ - "capacity":2 -}` - res, err := srv.Client().Post(srv.URL+"/api/v1/jam", "application/json", strings.NewReader(payload)) - is.NoErr(err) // create a new pool + { + "capacity": 0 + }` + + res, _ := srv.Client().Post(srv.URL+"/api/v1/jam", "application/json", strings.NewReader(payload)) is.Equal(res.StatusCode, http.StatusCreated) // created a new resource loc, err := res.Location() @@ -47,27 +46,73 @@ func TestService(t *testing.T) { firstPool = resource(loc.Path) }) - t.Run(`connect users to room websocket`, func(t *testing.T) { - c1, err := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/jam/"+firstPool) - is.NoErr(err) // connect client 1 - c2, err := w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/jam/"+firstPool) - is.NoErr(err) // connect client 2 - - t.Cleanup(func() { - c1.Close() - c2.Close() - }) + t.Run(`connect to jam session`, func(t *testing.T) { + c1, _, err := websocket.DefaultDialer.Dial(stripPrefix(srv.URL+"/ws/jam/"+firstPool), nil) + is.NoErr(err) // found first jam Session - _, err = w2.Dial(context.Background(), "ws"+strings.TrimPrefix(srv.URL, "http")+"/ws/jam/"+firstPool) - is.True(err != nil) // cannot connect client 3 + t.Cleanup(func() { c1.Close() }) - err = c1.WriteString("Hello, World!") - is.NoErr(err) // write string to pool + err = c1.WriteMessage(websocket.TextMessage, []byte("Hello, World!")) + is.NoErr(err) // write to pool - msg, err := c2.ReadString() - is.NoErr(err) // read message sent by c1 - - is.Equal(msg, "Hello, World!") + _, data, err := c1.ReadMessage() + is.NoErr(err) // read from pool + is.Equal(string(data), "Hello, World!") }) } + +// func TestWebsocket(t *testing.T) { +// // t.Parallel() +// is := is.New(t) + +// h := NewService(context.Background(), chi.NewMux()) +// srv := httptest.NewServer(h) + +// t.Cleanup(func() { srv.Close() }) + +// t.Run(`list all jam sessions`, func(t *testing.T) { +// r, _ := srv.Client().Get(srv.URL + "/api/v1/jam") +// is.Equal(r.StatusCode, http.StatusOK) // successfully created a new room +// }) + +// var firstPool string +// t.Run(`create a new room`, func(t *testing.T) { +// payload := ` +// { +// "capacity":2 +// }` +// res, err := srv.Client().Post(srv.URL+"/api/v1/jam", "application/json", strings.NewReader(payload)) +// is.NoErr(err) // create a new pool +// is.Equal(res.StatusCode, http.StatusCreated) // created a new resource + +// loc, err := res.Location() +// is.NoErr(err) // retrieve location + +// firstPool = resource(loc.Path) +// }) + +// t.Run(`connect users to room websocket`, func(t *testing.T) { +// c1, err := w2.Dial(context.Background(), stripPrefix(srv.URL+"/ws/jam/")+firstPool) +// is.NoErr(err) // connect client 1 +// c2, err := w2.Dial(context.Background(), stripPrefix(srv.URL+"/ws/jam/")+firstPool) +// is.NoErr(err) // connect client 2 + +// t.Cleanup(func() { +// c1.Close() +// c2.Close() +// }) + +// _, err = w2.Dial(context.Background(), stripPrefix(srv.URL+"/ws/jam/")+firstPool) +// is.True(err != nil) // cannot connect client 3 + +// err = c1.WriteString("Hello, World!") +// is.NoErr(err) // write string to pool + +// msg, err := c2.ReadString() +// is.NoErr(err) // read message sent by c1 + +// is.Equal(msg, "Hello, World!") + +// }) +// } diff --git a/service/jam/websocket.go b/service/jam/websocket.go new file mode 100644 index 00000000..8e25cf15 --- /dev/null +++ b/service/jam/websocket.go @@ -0,0 +1,212 @@ +package jam + +import ( + "errors" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +// default values +const ( + // Maximum message size allowed from peer. + readBufferSize = 512 + // Time allowed to read the next pong message from the peer. + readDeadline = 60 * time.Second + // Time allowed to write a message to the peer. + writeDeadline = 10 * time.Second +) + +var defaultUpgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +func UpgradeHTTP(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) { + return defaultUpgrader.Upgrade(w, r, nil) +} + +type Pool struct { + // for concurrent handling + mu sync.Mutex + // map for clients being used as a Set + cs map[*connHandler]struct{} + + // maximum cap clients allowed + Capacity uint + // Maximum message size allowed from peer. + ReadBufferSize int64 + // Time allowed to read the next pong message from the peer. + ReadTimeout time.Duration + // Time allowed to write a message to the peer. + WriteTimeout time.Duration + + // Send os + send chan Packet + r, d chan *connHandler + err chan error + + isServing bool +} + +// ListenAndServe listens to incoming websocket +func (pool *Pool) ListenAndServe(rwc *websocket.Conn) { + if !pool.isServing { + // Setup channels + pool.r = make(chan *connHandler) + pool.d = make(chan *connHandler) + pool.cs = make(map[*connHandler]struct{}) + pool.err = make(chan error) + pool.send = make(chan Packet) + + if pool.ReadBufferSize == 0 { + pool.ReadBufferSize = readBufferSize + } + + if pool.ReadTimeout == 0 { + pool.ReadTimeout = readDeadline + } + + if pool.WriteTimeout == 0 { + pool.WriteTimeout = writeDeadline + } + + go listen(pool) + pool.isServing = true + } + + ch := &connHandler{send: make(chan Packet), pool: pool, rwc: rwc} + pool.r <- ch + + go read(ch) + go write(ch) +} + +func listen(pool *Pool) { + for { + select { + case ch := <-pool.r: + if !pool.isFull() { + pool.cs[ch] = struct{}{} + } else { + // TODO write error to client + // ch.respondError(websocket.CloseInternalServerErr, errors.New("pool is full, connection closed")) + close(ch.send) + } + case ch := <-pool.d: + if _, ok := pool.cs[ch]; ok { + delete(pool.cs, ch) + close(ch.send) + } + case p := <-pool.send: + for ch := range pool.cs { + select { + case ch.send <- p: + default: + close(ch.send) + delete(pool.cs, ch) + } + } + // NOTE can this be cleaned up? + case err := <-pool.err: + if err != nil { + pool.Close() + } + } + } +} + +func (pool *Pool) Close() { + pool.mu.Lock() + { + for ch := range pool.cs { + pool.d <- ch + } + } + pool.mu.Unlock() + + pool.isServing = false + close(pool.d) + close(pool.r) + close(pool.send) + close(pool.err) +} + +func (pool *Pool) isFull() bool { return int(pool.Capacity) == len(pool.cs) } + +// func (pool *Pool) isEmpty() bool { return len(pool.cs) == 0 } + +type connHandler struct { + send chan Packet + pool *Pool + + rwc *websocket.Conn +} + +func (ch *connHandler) respond(typ int, data []byte) error { + return ch.rwc.WriteMessage(typ, data) +} + +func (ch *connHandler) respondError(code int, reason error) error { + var msg []byte + if reason != nil { + msg = websocket.FormatCloseMessage(code, reason.Error()) + } else { + msg = websocket.FormatCloseMessage(code, "") + } + return ch.respond(websocket.CloseMessage, msg) +} + +func read(ch *connHandler) { + defer func() { + ch.pool.d <- ch + ch.rwc.Close() + }() + + ch.rwc.SetReadLimit(ch.pool.ReadBufferSize) + ch.rwc.SetReadDeadline(time.Now().Add(ch.pool.ReadTimeout)) + + for { + typ, p, err := ch.rwc.ReadMessage() + if err != nil { + ch.respondError(websocket.CloseInternalServerErr, nil) + break + } + + ch.pool.send <- Packet{typ, p} + } +} + +func write(ch *connHandler) { + defer func() { + // ticker.Stop() + ch.rwc.Close() + }() + + for { + select { + case p, ok := <-ch.send: + ch.rwc.SetWriteDeadline(time.Now().Add(ch.pool.WriteTimeout)) + if !ok { + // the Pool closed ch.send + ch.respondError(websocket.CloseInternalServerErr, errors.New("pool closed connection")) + return + } + + typ, data := p.parse() + if err := ch.rwc.WriteMessage(typ, data); err != nil { + ch.respondError(websocket.CloseInternalServerErr, err) + return + } + } + } +} + +type Packet struct { + typ int + data []byte +} + +func (p Packet) parse() (int, []byte) { return p.typ, p.data } From 9995048d2ab3a840dd1b266a5d333409522b1c43 Mon Sep 17 00:00:00 2001 From: aboublef Date: Sun, 6 Nov 2022 08:44:30 +0000 Subject: [PATCH 216/246] hyphengolang/prelude v0.1.0 -> v0.1.3 --- go.mod | 2 +- go.sum | 4 +- internal/commands/run.go | 2 +- service/auth/service_test.go | 2 +- service/jam/service.go | 14 ++- service/jam/service_test.go | 2 +- service/jam/websocket.go | 212 ----------------------------------- 7 files changed, 17 insertions(+), 221 deletions(-) delete mode 100644 service/jam/websocket.go diff --git a/go.mod b/go.mod index 81c80a29..7ee32ec3 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/gobwas/ws v1.1.0 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 - github.com/hyphengolang/prelude v0.1.1 + github.com/hyphengolang/prelude v0.1.3 github.com/lithammer/shortuuid/v4 v4.0.0 github.com/manifoldco/promptui v0.9.0 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index dee44a82..06fab041 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hyphengolang/prelude v0.1.1 h1:L8JUlrdowWfcwzZKaVctdWdS7K4SiTVG+nJazOluPVY= -github.com/hyphengolang/prelude v0.1.1/go.mod h1:O1Wj9q3gP0zJwsrLQKvE1hyVz9fZIwIsh+d7P8wOgOc= +github.com/hyphengolang/prelude v0.1.3 h1:rNwjyywvCXd7/llM9R3lx7au9BYJvG0JzI2UO0j3yEY= +github.com/hyphengolang/prelude v0.1.3/go.mod h1:O1Wj9q3gP0zJwsrLQKvE1hyVz9fZIwIsh+d7P8wOgOc= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= diff --git a/internal/commands/run.go b/internal/commands/run.go index 035ccbb9..5529b7cb 100644 --- a/internal/commands/run.go +++ b/internal/commands/run.go @@ -258,7 +258,7 @@ func serve(cfg *config.Config) error { } // init application store - s := store.New(sCtx, "") // needs fix + s, _ := store.New(sCtx, "") // needs fix // setup a new handler h := service.New(sCtx, s) diff --git a/service/auth/service_test.go b/service/auth/service_test.go index 0a5943a3..e3cfdfda 100644 --- a/service/auth/service_test.go +++ b/service/auth/service_test.go @@ -12,7 +12,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/hyphengolang/prelude/testing/is" "github.com/rog-golang-buddies/rmx/store/auth" - "github.com/rog-golang-buddies/rmx/store/sql/user" + "github.com/rog-golang-buddies/rmx/store/user" ) const applicationJson = "application/json" diff --git a/service/jam/service.go b/service/jam/service.go index f61b75e3..62da4622 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -10,6 +10,7 @@ import ( "github.com/go-chi/chi/v5" h "github.com/hyphengolang/prelude/http" + "github.com/hyphengolang/prelude/http/websocket" "github.com/rog-golang-buddies/rmx/internal/suid" ws "github.com/rog-golang-buddies/rmx/internal/websocket" @@ -46,7 +47,7 @@ type Service struct { type muxEntry struct { sid suid.UUID - pool *Pool + pool *websocket.Pool } func (e muxEntry) String() string { return e.sid.ShortUUID().String() } @@ -64,7 +65,7 @@ func (mux *mux) Store(e muxEntry) { mux.mu.Unlock() } -func (mux *mux) Load(sid suid.UUID) (pool *Pool, err error) { +func (mux *mux) Load(sid suid.UUID) (pool *websocket.Pool, err error) { mux.mu.Lock() e, ok := mux.mp[sid] mux.mu.Unlock() @@ -96,6 +97,7 @@ func (s *Service) routes() { } func (s *Service) handleP2PComms(mux *mux) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { // decode uuid from sid, err := s.parseUUID(w, r) @@ -110,6 +112,12 @@ func (s *Service) handleP2PComms(mux *mux) http.HandlerFunc { return } + // if pool is full then reject + if pool.IsFull() { + // TODO error handling + return + } + rwc, err := UpgradeHTTP(w, r) if err != nil { s.respond(w, r, err, http.StatusUpgradeRequired) @@ -132,7 +140,7 @@ func (s *Service) handleCreateJamRoom(mux *mux) http.HandlerFunc { return } - pool := &Pool{ + pool := &websocket.Pool{ Capacity: pl.Capacity, } diff --git a/service/jam/service_test.go b/service/jam/service_test.go index 99ffb13b..a1b48852 100644 --- a/service/jam/service_test.go +++ b/service/jam/service_test.go @@ -34,7 +34,7 @@ func TestService(t *testing.T) { t.Run(`create a new Room`, func(t *testing.T) { payload := ` { - "capacity": 0 + "capacity": 2 }` res, _ := srv.Client().Post(srv.URL+"/api/v1/jam", "application/json", strings.NewReader(payload)) diff --git a/service/jam/websocket.go b/service/jam/websocket.go deleted file mode 100644 index 8e25cf15..00000000 --- a/service/jam/websocket.go +++ /dev/null @@ -1,212 +0,0 @@ -package jam - -import ( - "errors" - "net/http" - "sync" - "time" - - "github.com/gorilla/websocket" -) - -// default values -const ( - // Maximum message size allowed from peer. - readBufferSize = 512 - // Time allowed to read the next pong message from the peer. - readDeadline = 60 * time.Second - // Time allowed to write a message to the peer. - writeDeadline = 10 * time.Second -) - -var defaultUpgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, -} - -func UpgradeHTTP(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) { - return defaultUpgrader.Upgrade(w, r, nil) -} - -type Pool struct { - // for concurrent handling - mu sync.Mutex - // map for clients being used as a Set - cs map[*connHandler]struct{} - - // maximum cap clients allowed - Capacity uint - // Maximum message size allowed from peer. - ReadBufferSize int64 - // Time allowed to read the next pong message from the peer. - ReadTimeout time.Duration - // Time allowed to write a message to the peer. - WriteTimeout time.Duration - - // Send os - send chan Packet - r, d chan *connHandler - err chan error - - isServing bool -} - -// ListenAndServe listens to incoming websocket -func (pool *Pool) ListenAndServe(rwc *websocket.Conn) { - if !pool.isServing { - // Setup channels - pool.r = make(chan *connHandler) - pool.d = make(chan *connHandler) - pool.cs = make(map[*connHandler]struct{}) - pool.err = make(chan error) - pool.send = make(chan Packet) - - if pool.ReadBufferSize == 0 { - pool.ReadBufferSize = readBufferSize - } - - if pool.ReadTimeout == 0 { - pool.ReadTimeout = readDeadline - } - - if pool.WriteTimeout == 0 { - pool.WriteTimeout = writeDeadline - } - - go listen(pool) - pool.isServing = true - } - - ch := &connHandler{send: make(chan Packet), pool: pool, rwc: rwc} - pool.r <- ch - - go read(ch) - go write(ch) -} - -func listen(pool *Pool) { - for { - select { - case ch := <-pool.r: - if !pool.isFull() { - pool.cs[ch] = struct{}{} - } else { - // TODO write error to client - // ch.respondError(websocket.CloseInternalServerErr, errors.New("pool is full, connection closed")) - close(ch.send) - } - case ch := <-pool.d: - if _, ok := pool.cs[ch]; ok { - delete(pool.cs, ch) - close(ch.send) - } - case p := <-pool.send: - for ch := range pool.cs { - select { - case ch.send <- p: - default: - close(ch.send) - delete(pool.cs, ch) - } - } - // NOTE can this be cleaned up? - case err := <-pool.err: - if err != nil { - pool.Close() - } - } - } -} - -func (pool *Pool) Close() { - pool.mu.Lock() - { - for ch := range pool.cs { - pool.d <- ch - } - } - pool.mu.Unlock() - - pool.isServing = false - close(pool.d) - close(pool.r) - close(pool.send) - close(pool.err) -} - -func (pool *Pool) isFull() bool { return int(pool.Capacity) == len(pool.cs) } - -// func (pool *Pool) isEmpty() bool { return len(pool.cs) == 0 } - -type connHandler struct { - send chan Packet - pool *Pool - - rwc *websocket.Conn -} - -func (ch *connHandler) respond(typ int, data []byte) error { - return ch.rwc.WriteMessage(typ, data) -} - -func (ch *connHandler) respondError(code int, reason error) error { - var msg []byte - if reason != nil { - msg = websocket.FormatCloseMessage(code, reason.Error()) - } else { - msg = websocket.FormatCloseMessage(code, "") - } - return ch.respond(websocket.CloseMessage, msg) -} - -func read(ch *connHandler) { - defer func() { - ch.pool.d <- ch - ch.rwc.Close() - }() - - ch.rwc.SetReadLimit(ch.pool.ReadBufferSize) - ch.rwc.SetReadDeadline(time.Now().Add(ch.pool.ReadTimeout)) - - for { - typ, p, err := ch.rwc.ReadMessage() - if err != nil { - ch.respondError(websocket.CloseInternalServerErr, nil) - break - } - - ch.pool.send <- Packet{typ, p} - } -} - -func write(ch *connHandler) { - defer func() { - // ticker.Stop() - ch.rwc.Close() - }() - - for { - select { - case p, ok := <-ch.send: - ch.rwc.SetWriteDeadline(time.Now().Add(ch.pool.WriteTimeout)) - if !ok { - // the Pool closed ch.send - ch.respondError(websocket.CloseInternalServerErr, errors.New("pool closed connection")) - return - } - - typ, data := p.parse() - if err := ch.rwc.WriteMessage(typ, data); err != nil { - ch.respondError(websocket.CloseInternalServerErr, err) - return - } - } - } -} - -type Packet struct { - typ int - data []byte -} - -func (p Packet) parse() (int, []byte) { return p.typ, p.data } From 54bfe90999335c92bc6a2f8c41c0038ae69c0468 Mon Sep 17 00:00:00 2001 From: aboublef Date: Sun, 6 Nov 2022 08:50:59 +0000 Subject: [PATCH 217/246] slightly cleaner --- service/jam/service.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/service/jam/service.go b/service/jam/service.go index 62da4622..e1ae23e6 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -6,6 +6,7 @@ import ( "log" "net/http" "sync" + "time" "github.com/go-chi/chi/v5" @@ -112,13 +113,12 @@ func (s *Service) handleP2PComms(mux *mux) http.HandlerFunc { return } - // if pool is full then reject - if pool.IsFull() { - // TODO error handling + if err := errors.New("pool has reached max capacity"); pool.IsFull() { + s.respond(w, r, err, http.StatusServiceUnavailable) return } - rwc, err := UpgradeHTTP(w, r) + rwc, err := websocket.UpgradeHTTP(w, r) if err != nil { s.respond(w, r, err, http.StatusUpgradeRequired) return @@ -141,7 +141,10 @@ func (s *Service) handleCreateJamRoom(mux *mux) http.HandlerFunc { } pool := &websocket.Pool{ - Capacity: pl.Capacity, + Capacity: pl.Capacity, + ReadBufferSize: 512, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, } e := muxEntry{suid.NewUUID(), pool} From afa1d2465c53ae2a56e73edbdaf364f9a3c8e4bf Mon Sep 17 00:00:00 2001 From: aboublef Date: Sun, 6 Nov 2022 09:09:26 +0000 Subject: [PATCH 218/246] removed dubplicated packages --- internal/suid/suid.go | 39 ------ internal/suid/suid_test.go | 38 ----- internal/websocket/client.go | 71 ---------- internal/websocket/conn.go | 39 ------ internal/websocket/errors.go | 6 - internal/websocket/pool.go | 134 ------------------ internal/websocket/x/README.md | 3 - internal/websocket/x/client.go | 49 ------- internal/websocket/x/pool/pool.go | 72 ---------- internal/websocket/x/websocket.go | 185 ------------------------- internal/websocket/x/websocket_test.go | 1 - 11 files changed, 637 deletions(-) delete mode 100644 internal/suid/suid.go delete mode 100644 internal/suid/suid_test.go delete mode 100644 internal/websocket/client.go delete mode 100644 internal/websocket/conn.go delete mode 100644 internal/websocket/errors.go delete mode 100644 internal/websocket/pool.go delete mode 100644 internal/websocket/x/README.md delete mode 100644 internal/websocket/x/client.go delete mode 100644 internal/websocket/x/pool/pool.go delete mode 100644 internal/websocket/x/websocket.go delete mode 100644 internal/websocket/x/websocket_test.go diff --git a/internal/suid/suid.go b/internal/suid/suid.go deleted file mode 100644 index f4fa6727..00000000 --- a/internal/suid/suid.go +++ /dev/null @@ -1,39 +0,0 @@ -package suid - -import ( - "github.com/google/uuid" - suid "github.com/lithammer/shortuuid/v4" -) - -// ? The API for UUID/SUID is still in development -type UUID struct{ uuid.UUID } - -func NewUUID() UUID { return UUID{uuid.New()} } - -func (u UUID) ShortUUID() SUID { return SUID(suid.DefaultEncoder.Encode(u.UUID)) } - -type SUID string - -func NewSUID() SUID { return SUID(suid.New()) } - -func FromUUID(uid UUID) SUID { return SUID(suid.DefaultEncoder.Encode(uid.UUID)) } - -func ParseString(s string) (UUID, error) { - uid, err := suid.DefaultEncoder.Decode(s) - return UUID{uid}, err -} - -func MustParse(s string) UUID { - uid, err := ParseString(s) - if err != nil { - panic(err) - } - return uid -} - -func (s SUID) String() string { return string(s) } - -func (s SUID) UUID() (UUID, error) { - u, err := suid.DefaultEncoder.Decode(string(s)) - return UUID{u}, err -} diff --git a/internal/suid/suid_test.go b/internal/suid/suid_test.go deleted file mode 100644 index 4015bb8e..00000000 --- a/internal/suid/suid_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package suid - -import ( - "encoding/json" - "strings" - "testing" -) - -func TestUUID(t *testing.T) { - uid := NewUUID() - - b, err := json.Marshal(uid.ShortUUID()) - if err != nil { - t.Fatal(err) - } - - var sid SUID - if err := json.NewDecoder(strings.NewReader(string(b))).Decode(&sid); err != nil { - t.Fatal(err) - } - - oid, err := sid.UUID() - if err != nil { - t.Fatal(err) - } - - if oid != uid { - t.Fatalf("expected: %s;got %s\n", uid, sid) - } -} - -func TestSIze(t *testing.T) { - s1 := NewSUID() - s2 := NewSUID() - s3 := NewSUID() - - t.Log(len(s1), len(s2), len(s3)) -} diff --git a/internal/websocket/client.go b/internal/websocket/client.go deleted file mode 100644 index 1d09b680..00000000 --- a/internal/websocket/client.go +++ /dev/null @@ -1,71 +0,0 @@ -package websocket - -import ( - "log" - "sync" - - "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/suid" - "golang.org/x/exp/maps" -) - -type Client struct { - mu sync.RWMutex - - ps map[suid.UUID]*Pool -} - -var DefaultClient = &Client{ - ps: make(map[suid.UUID]*Pool), -} - -func NewClient() *Client { - c := &Client{ - ps: make(map[suid.UUID]*Pool), - } - - return c -} - -func (c *Client) Size() int { return len(c.ps) } - -func (c *Client) Close() error { - return internal.ErrNotImplemented -} - -func (c *Client) NewPool(maxCount int) (suid.UUID, error) { - c.mu.Lock() - defer c.mu.Unlock() - - p := NewPool(maxCount) - - c.ps[p.ID] = p - - return p.ID, nil -} - -func (c *Client) Get(uid suid.UUID) (*Pool, error) { - c.mu.Lock() - defer c.mu.Unlock() - - for id, p := range c.ps { - log.Println("is match?", id, uid, id == uid) - if id == uid { - return p, nil - } - } - - return nil, ErrNoPool -} - -func (c *Client) Has(uid suid.UUID) bool { - _, err := c.Get(uid) - return err == nil -} - -func (c *Client) List() []*Pool { - c.mu.Lock() - defer c.mu.Unlock() - - return maps.Values(c.ps) -} diff --git a/internal/websocket/conn.go b/internal/websocket/conn.go deleted file mode 100644 index 7085b70c..00000000 --- a/internal/websocket/conn.go +++ /dev/null @@ -1,39 +0,0 @@ -package websocket - -import ( - "github.com/gorilla/websocket" - rmx "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/suid" -) - -type Conn struct { - ID suid.UUID - - rwc *websocket.Conn - p *Pool -} - -func (c Conn) Pool() *Pool { return c.p } - -func (c Conn) Close() error { - c.p.Delete(c.ID) - - return c.rwc.Close() -} - -func (c Conn) ReadJSON(v any) error { return c.rwc.ReadJSON(v) } - -func (c Conn) WriteJSON(v any) error { return c.rwc.WriteJSON(v) } - -func (c Conn) SendMessage(v any) error { - c.p.msgs <- v - return nil -} - -func (c Conn) SendMessage2(typ rmx.MsgTyp, data any) error { - v := struct { - Typ rmx.MsgTyp - }{} - c.p.msgs <- v - return nil -} diff --git a/internal/websocket/errors.go b/internal/websocket/errors.go deleted file mode 100644 index 3dc87b24..00000000 --- a/internal/websocket/errors.go +++ /dev/null @@ -1,6 +0,0 @@ -package websocket - -import "errors" - -var ErrNoPool = errors.New("ws: pool does not exist") -var ErrMaxConn = errors.New("ws: maximum number of connections reached") diff --git a/internal/websocket/pool.go b/internal/websocket/pool.go deleted file mode 100644 index 97192cf2..00000000 --- a/internal/websocket/pool.go +++ /dev/null @@ -1,134 +0,0 @@ -package websocket - -import ( - "net/http" - "sync" - - "github.com/gorilla/websocket" - "github.com/rog-golang-buddies/rmx/internal/suid" - - // https://stackoverflow.com/questions/21362950/getting-a-slice-of-keys-from-a-map - - "golang.org/x/exp/maps" -) - -type Pool struct { - mu sync.RWMutex - - ID suid.UUID - MaxConn int - - cs map[suid.UUID]*Conn - msgs chan any -} - -func NewPool(maxCount int) *Pool { - p := &Pool{ - ID: suid.NewUUID(), - MaxConn: maxCount, - cs: make(map[suid.UUID]*Conn), - msgs: make(chan any), - } - - go func() { - defer p.Close() - - for msg := range p.msgs { - for _, c := range p.cs { - c.WriteJSON(msg) - } - } - }() - - return p -} - -// maybe should just be a variable -func DefaultPool() *Pool { - p := &Pool{ - ID: suid.NewUUID(), - MaxConn: 4, - cs: make(map[suid.UUID]*Conn), - msgs: make(chan any), - } - - go func() { - defer p.Close() - - for msg := range p.msgs { - for _, c := range p.cs { - c.WriteJSON(msg) - } - } - - // for _, c := range p.cs { c.WriteJSON(<-p.msgs) } - }() - - return p -} - -func (p *Pool) Size() int { - p.mu.Lock() - defer p.mu.Unlock() - - return len(p.cs) -} - -func (p *Pool) Keys() []suid.UUID { - p.mu.Lock() - defer p.mu.Unlock() - - return maps.Keys(p.cs) -} - -func (p *Pool) NewConn(w http.ResponseWriter, r *http.Request, u *websocket.Upgrader) (*Conn, error) { - p.mu.Lock() - defer p.mu.Unlock() - - rwc, err := u.Upgrade(w, r, nil) - if err != nil { - return nil, err - } - - c := &Conn{suid.NewUUID(), rwc, p} - - p.cs[c.ID] = c - - return c, nil -} - -func (p *Pool) Delete(uid suid.UUID) { - p.mu.Lock() - defer p.mu.Unlock() - - delete(p.cs, uid) -} - -func (p *Pool) Close() error { - p.mu.Lock() - defer p.mu.Unlock() - - for _, c := range p.cs { - if err := c.Close(); err != nil { - return err - } - } - - return nil -} - -/* -Experimental: Pub/Sub pattern - -func (p *Pool) AddEventListener(eventTyp string, callback func(event any)) { - if eventTyp already exists then panic - if callback is nil then panic - - type entry struct { eventTyp string; callback func(event any); } - add entry to to map[eventType]entry -} - -func (p *Pool) Listen() { - -} -*/ diff --git a/internal/websocket/x/README.md b/internal/websocket/x/README.md deleted file mode 100644 index 06a36c85..00000000 --- a/internal/websocket/x/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Websockets - -Inspiration taken from [here](https://medium.com/free-code-camp/million-websockets-and-go-cc58418460bb) diff --git a/internal/websocket/x/client.go b/internal/websocket/x/client.go deleted file mode 100644 index e5921242..00000000 --- a/internal/websocket/x/client.go +++ /dev/null @@ -1,49 +0,0 @@ -package websocket - -import ( - "sync" - - "github.com/rog-golang-buddies/rmx/internal/suid" -) - -type Multiplexer struct { - ps map[suid.SUID]*Pool -} - -func (mux *Multiplexer) Append(sid suid.SUID, pool Pool) {} - -type Pool struct { - Capacity uint - - seq uint - cs sync.Map -} - -func (p *Pool) Append(conn Conn) { - p.cs.Store(conn, struct{}{}) - p.seq++ -} - -func (p *Pool) IsCap() bool { return p.seq >= p.Capacity } - -func (p *Pool) Remove(conn Conn) error { - p.cs.Delete(conn) - p.seq-- - return conn.Close() -} - -// General broadcast method that can be used with any message type -func (p *Pool) Broadcast(msg any) { - var f func(key, value any) bool - - switch msg := msg.(type) { - case []byte: - f = func(key, value any) bool { return key.(Conn).Write(msg) == nil } - case string: - f = func(key, value any) bool { return key.(Conn).WriteString(msg) == nil } - default: - f = func(key, value any) bool { return key.(Conn).WriteJSON(msg) == nil } - } - - p.cs.Range(f) -} diff --git a/internal/websocket/x/pool/pool.go b/internal/websocket/x/pool/pool.go deleted file mode 100644 index 6e22b011..00000000 --- a/internal/websocket/x/pool/pool.go +++ /dev/null @@ -1,72 +0,0 @@ -package pool - -import ( - "fmt" - "time" -) - -// ErrScheduleTimeout returned by Pool to indicate that there no free -// goroutines during some period of time. -var ErrScheduleTimeout = fmt.Errorf("schedule error: timed out") - -// Pool contains logic of goroutine reuse. -type Pool struct { - sem chan struct{} - work chan func() -} - -// NewPool creates new goroutine pool with given size. It also creates a work -// queue of given size. Finally, it spawns given amount of goroutines -// immediately. -func NewPool(size, queue, spawn int) *Pool { - if spawn <= 0 && queue > 0 { - panic("dead queue configuration detected") - } - - if spawn > size { - panic("spawn > workers") - } - p := &Pool{ - sem: make(chan struct{}, size), - work: make(chan func(), queue), - } - for i := 0; i < spawn; i++ { - p.sem <- struct{}{} - go p.worker(func() {}) - } - - return p -} - -// Schedule schedules task to be executed over pool's workers. -func (p *Pool) Schedule(task func()) { - p.schedule(task, nil) -} - -// ScheduleTimeout schedules task to be executed over pool's workers. -// It returns ErrScheduleTimeout when no free workers met during given timeout. -func (p *Pool) ScheduleTimeout(timeout time.Duration, task func()) error { - return p.schedule(task, time.After(timeout)) -} - -func (p *Pool) schedule(task func(), timeout <-chan time.Time) error { - select { - case <-timeout: - return ErrScheduleTimeout - case p.work <- task: - return nil - case p.sem <- struct{}{}: - go p.worker(task) - return nil - } -} - -func (p *Pool) worker(task func()) { - defer func() { <-p.sem }() - - task() - - for task := range p.work { - task() - } -} diff --git a/internal/websocket/x/websocket.go b/internal/websocket/x/websocket.go deleted file mode 100644 index 55ab83f6..00000000 --- a/internal/websocket/x/websocket.go +++ /dev/null @@ -1,185 +0,0 @@ -package websocket - -import ( - "context" - "encoding/json" - "io" - "net" - "net/http" - - "github.com/gobwas/ws" - "github.com/gobwas/ws/wsutil" -) - -// Connection - -type Conn interface { - Reader - Writer - Closer -} - -type ReaderCloser interface { - Reader - Closer -} - -type WriterCloser interface { - Writer - Closer -} - -type Closer interface { - Close() error -} - -type Reader interface { - State() ws.State - - Read() ([]byte, error) - ReadString() (string, error) - ReadJSON(v any) error -} - -type Writer interface { - Close() error - State() ws.State - - Write(p []byte) error - WriteString(p string) error - WriteJSON(v any) error -} - -type serverConn struct { - r *wsutil.Reader - w *wsutil.Writer - - rwc net.Conn -} - -func (c *serverConn) State() ws.State { return ws.StateServerSide } - -func (c *serverConn) Close() error { return c.rwc.Close() } - -func (c *serverConn) Read() ([]byte, error) { - b, err := wsutil.ReadClientBinary(c.rwc) - return b, err -} - -func (c *serverConn) ReadString() (string, error) { - h, err := c.r.NextFrame() - if err != nil { - return "", err - } - - // Reset writer to write frame with right operation code. - c.w.Reset(c.rwc, c.State(), h.OpCode) - - b, err := io.ReadAll(c.r) - return string(b), err -} - -func (c *serverConn) ReadJSON(v any) error { - h, err := c.r.NextFrame() - if err != nil { - return err - } - - if h.OpCode == ws.OpClose { - return io.EOF - } - return json.NewDecoder(c.r).Decode(v) -} - -func (c *serverConn) Write(p []byte) error { - return wsutil.WriteMessage(c.rwc, c.State(), ws.OpBinary, p) -} - -func (c *serverConn) WriteString(s string) error { - _, err := io.WriteString(c.w, s) - if err != nil { - return err - } - return c.w.Flush() -} - -func (c *serverConn) WriteJSON(v any) error { - if err := json.NewEncoder(c.w).Encode(v); err != nil { - return err - } - return c.w.Flush() -} - -func UpgradeHTTP(w http.ResponseWriter, r *http.Request) (conn Conn, err error) { - rwc, _, _, err := ws.UpgradeHTTP(r, w) - c := &serverConn{rwc: rwc} - c.r = wsutil.NewReader(rwc, c.State()) - c.w = wsutil.NewWriter(rwc, c.State(), ws.OpText) - return c, err -} - -type clientConn struct { - r *wsutil.Reader - w *wsutil.Writer - - rwc net.Conn -} - -func (c *clientConn) State() ws.State { return ws.StateClientSide } - -func (c *clientConn) Close() error { return c.rwc.Close() } - -func (c *clientConn) Read() ([]byte, error) { - b, err := wsutil.ReadServerBinary(c.rwc) - return b, err -} - -func (c *clientConn) ReadString() (string, error) { - h, err := c.r.NextFrame() - if err != nil { - return "", err - } - // Reset writer to write frame with right operation code. - c.w.Reset(c.rwc, c.State(), h.OpCode) - - b, err := io.ReadAll(c.r) - return string(b), err -} - -func (c *clientConn) ReadJSON(v any) error { - h, err := c.r.NextFrame() - if err != nil { - return err - } - if h.OpCode == ws.OpClose { - return io.EOF - } - return json.NewDecoder(c.r).Decode(v) -} - -func (c *clientConn) Write(p []byte) error { - return wsutil.WriteMessage(c.rwc, c.State(), ws.OpBinary, p) -} - -func (c *clientConn) WriteString(s string) error { - _, err := io.WriteString(c.w, s) - if err != nil { - return err - } - return c.w.Flush() -} - -func (c *clientConn) WriteJSON(v any) error { - if err := json.NewEncoder(c.w).Encode(v); err != nil { - return err - } - return c.w.Flush() -} - -func Dial(ctx context.Context, urlStr string) (conn Conn, err error) { - rwc, _, _, err := ws.Dial(context.Background(), urlStr) - c := &clientConn{rwc: rwc} - c.r = wsutil.NewReader(rwc, c.State()) - c.w = wsutil.NewWriter(rwc, c.State(), ws.OpText) - return c, err -} diff --git a/internal/websocket/x/websocket_test.go b/internal/websocket/x/websocket_test.go deleted file mode 100644 index 708bc8cb..00000000 --- a/internal/websocket/x/websocket_test.go +++ /dev/null @@ -1 +0,0 @@ -package websocket From 9a5679aff62d9c435b4c2a713e0ad03fc8212e6c Mon Sep 17 00:00:00 2001 From: aboublef Date: Sun, 6 Nov 2022 09:10:32 +0000 Subject: [PATCH 219/246] hotfix services --- service/auth/service.go | 3 +-- service/jam/service.go | 8 +++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/service/auth/service.go b/service/auth/service.go index 0d431a5a..fa6acef0 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -15,10 +15,9 @@ import ( "github.com/hyphengolang/prelude/types/password" // github.com/rog-golang-buddies/rmx/service/internal/auth/auth + "github.com/hyphengolang/prelude/types/suid" "github.com/rog-golang-buddies/rmx/internal" "github.com/rog-golang-buddies/rmx/internal/auth" - "github.com/rog-golang-buddies/rmx/internal/suid" - // big no-no ) var ( diff --git a/service/jam/service.go b/service/jam/service.go index e1ae23e6..a69a1a00 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -13,8 +13,7 @@ import ( h "github.com/hyphengolang/prelude/http" "github.com/hyphengolang/prelude/http/websocket" - "github.com/rog-golang-buddies/rmx/internal/suid" - ws "github.com/rog-golang-buddies/rmx/internal/websocket" + "github.com/hyphengolang/prelude/types/suid" ) // Jam Service Endpoints @@ -36,7 +35,7 @@ import ( // GET /ws/jam/{uuid} type Service struct { m chi.Router - c *ws.Client + // c *ws.Client log func(s ...any) logf func(string, ...any) @@ -226,7 +225,7 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeH func NewService(ctx context.Context, r chi.Router) *Service { s := &Service{ r, - ws.DefaultClient, log.Print, log.Printf, h.Created, h.Respond, h.Decode, + log.Print, log.Printf, h.Created, h.Respond, h.Decode, } s.routes() return s @@ -239,7 +238,6 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, type Jam struct { Name string `json:"name"` BPM int `json:"bpm"` - ws.Pool } type Session struct { From ef3cafb65728db11eaa62364fe2b14f897daeb1c Mon Sep 17 00:00:00 2001 From: aboublef Date: Sun, 6 Nov 2022 09:10:48 +0000 Subject: [PATCH 220/246] hotfix user_repo --- store/user/repo.go | 2 +- store/user/repo_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/store/user/repo.go b/store/user/repo.go index 2f86d74c..6983217d 100644 --- a/store/user/repo.go +++ b/store/user/repo.go @@ -9,12 +9,12 @@ import ( psql "github.com/hyphengolang/prelude/sql/postgres" "github.com/hyphengolang/prelude/types/email" "github.com/hyphengolang/prelude/types/password" + "github.com/hyphengolang/prelude/types/suid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/suid" ) const ( diff --git a/store/user/repo_test.go b/store/user/repo_test.go index f39c0419..3c99daf6 100644 --- a/store/user/repo_test.go +++ b/store/user/repo_test.go @@ -8,9 +8,9 @@ import ( "github.com/hyphengolang/prelude/testing/is" "github.com/hyphengolang/prelude/types/email" "github.com/hyphengolang/prelude/types/password" + "github.com/hyphengolang/prelude/types/suid" "github.com/jackc/pgx/v5/pgxpool" "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/suid" ) /* From 7ce7e4ff452bd34684d5565f82880032d5d8f96e Mon Sep 17 00:00:00 2001 From: aboublef Date: Sun, 6 Nov 2022 09:11:33 +0000 Subject: [PATCH 221/246] go tests pass --- internal/auth/auth_test.go | 2 +- internal/internal.go | 40 ++++++++++++++++++++++++-------------- ui/terminal/tui/tui.go | 3 ++- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index bc616d08..e1e2392c 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -8,10 +8,10 @@ import ( "github.com/hyphengolang/prelude/testing/is" "github.com/hyphengolang/prelude/types/email" "github.com/hyphengolang/prelude/types/password" + "github.com/hyphengolang/prelude/types/suid" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwt" "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/suid" ) func TestToken(t *testing.T) { diff --git a/internal/internal.go b/internal/internal.go index 7b66e416..47e87a7e 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -7,7 +7,7 @@ import ( "github.com/hyphengolang/prelude/types/email" "github.com/hyphengolang/prelude/types/password" - "github.com/rog-golang-buddies/rmx/internal/suid" + "github.com/hyphengolang/prelude/types/suid" ) var ( @@ -111,41 +111,51 @@ type WTokenClient interface { BlackListRefreshToken(ctx context.Context, token string) error } +type TokenReader interface { + ValidateRefreshToken(ctx context.Context, token string) error + ValidateClientID(ctx context.Context, cid string) error +} + +type TokenWriter interface { + BlackListClientID(ctx context.Context, cid, email string) error + BlackListRefreshToken(ctx context.Context, token string) error +} + type RWUserRepo interface { - UserCloser - UserReader - UserWriter + RepoCloser + RepoReader[User] + RepoWriter[User] } type WUserRepo interface { - UserCloser - UserWriter + RepoCloser + RepoWriter[User] } type UserRepo interface { - UserCloser - UserReader + RepoCloser + RepoReader[User] } -type UserReader interface { +type RepoReader[Entry any] interface { // Returns an array of users subject to any filter // conditions that are required - SelectMany(ctx context.Context) ([]User, error) + SelectMany(ctx context.Context) ([]Entry, error) // Returns a user form the database, the "key" // can be either the "id", "email" or "username" // as these are all given unique values - Select(ctx context.Context, key any) (*User, error) + Select(ctx context.Context, key any) (*Entry, error) } -type UserWriter interface { - // Insert a new user to the database - Insert(ctx context.Context, u *User) error +type RepoWriter[Entry any] interface { + // Insert a new item to the database + Insert(ctx context.Context, e *Entry) error // Performs a "hard" delete from database // Restricted to admin only Delete(ctx context.Context, key any) error } -type UserCloser interface { +type RepoCloser interface { Close() } diff --git a/ui/terminal/tui/tui.go b/ui/terminal/tui/tui.go index 94b79896..00d6b3d7 100644 --- a/ui/terminal/tui/tui.go +++ b/ui/terminal/tui/tui.go @@ -7,7 +7,8 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/gorilla/websocket" - "github.com/rog-golang-buddies/rmx/internal/suid" + "github.com/hyphengolang/prelude/types/suid" + "github.com/rog-golang-buddies/rmx/ui/terminal/tui/jamui" "github.com/rog-golang-buddies/rmx/ui/terminal/tui/lobbyui" ) From 5f3427562aa79ae44097b9f8feef08520c57b77a Mon Sep 17 00:00:00 2001 From: aboublef Date: Mon, 12 Dec 2022 18:59:01 +0000 Subject: [PATCH 222/246] pkg has auth, service and test pacakges --- {internal => pkg}/auth/auth.go | 0 {internal => pkg}/auth/auth_test.go | 0 pkg/repotest/user.go | 135 ++++++++++++++++++++++++++++ pkg/service/service.go | 66 ++++++++++++++ 4 files changed, 201 insertions(+) rename {internal => pkg}/auth/auth.go (100%) rename {internal => pkg}/auth/auth_test.go (100%) create mode 100644 pkg/repotest/user.go create mode 100644 pkg/service/service.go diff --git a/internal/auth/auth.go b/pkg/auth/auth.go similarity index 100% rename from internal/auth/auth.go rename to pkg/auth/auth.go diff --git a/internal/auth/auth_test.go b/pkg/auth/auth_test.go similarity index 100% rename from internal/auth/auth_test.go rename to pkg/auth/auth_test.go diff --git a/pkg/repotest/user.go b/pkg/repotest/user.go new file mode 100644 index 00000000..17c7b628 --- /dev/null +++ b/pkg/repotest/user.go @@ -0,0 +1,135 @@ +package repotest + +import ( + "context" + "log" + "sync" + "time" + + "github.com/hyphengolang/prelude/types/email" + "github.com/hyphengolang/prelude/types/password" + "github.com/hyphengolang/prelude/types/suid" + + "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/store/user" +) + +type UserRepo interface { + user.Repo +} + +func (r *repo) Close() {} + +func (r *repo) Delete(ctx context.Context, key any) error { return nil } + +type repo struct { + mu sync.Mutex + miu map[suid.UUID]*user.User + mei map[string]*user.User + + log func(v ...any) + logf func(format string, v ...any) +} + +func NewUserRepo() UserRepo { + r := &repo{ + miu: make(map[suid.UUID]*user.User), + mei: make(map[string]*user.User), + log: log.Println, + logf: log.Printf, + } + + return r +} + +// Remove implements internal.UserRepo +func (r *repo) Remove(ctx context.Context, key any) error { + panic("unimplemented") +} + +func (r *repo) Insert(ctx context.Context, iu *internal.User) error { + r.mu.Lock() + defer r.mu.Unlock() + + if _, found := r.mei[iu.Email.String()]; found { + return internal.ErrAlreadyExists + } + + u := &user.User{ + ID: iu.ID, + Username: iu.Username, + Email: iu.Email, + Password: iu.Password, + CreatedAt: time.Now(), + } + r.mei[iu.Email.String()], r.miu[iu.ID] = u, u + + return nil +} + +func (r *repo) SelectMany(ctx context.Context) ([]internal.User, error) { + panic("not implemented") +} + +func (r *repo) Select(ctx context.Context, key any) (*internal.User, error) { + switch key := key.(type) { + case suid.UUID: + return r.selectUUID(key) + case email.Email: + return r.selectEmail(key) + case string: + return r.selectUsername(key) + default: + return nil, internal.ErrInvalidType + } +} + +func (r *repo) selectUUID(uid suid.UUID) (*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() + + if u, ok := r.miu[uid]; ok { + return &internal.User{ + ID: u.ID, + Username: u.Username, + Email: email.Email(u.Email), + Password: password.PasswordHash(u.Password), + }, nil + } + + return nil, internal.ErrNotFound +} + +func (r *repo) selectUsername(username string) (*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() + + for _, u := range r.mei { + if u.Username == username { + return &internal.User{ + ID: u.ID, + Username: u.Username, + Email: email.Email(u.Email), + Password: password.PasswordHash(u.Password), + }, nil + } + } + + return nil, internal.ErrNotFound +} + +func (r *repo) selectEmail(email email.Email) (*internal.User, error) { + r.mu.Lock() + defer r.mu.Unlock() + + if u, ok := r.mei[email.String()]; ok { + return &internal.User{ + ID: u.ID, + Username: u.Username, + Email: u.Email, + Password: password.PasswordHash(u.Password), + }, nil + } + + return nil, internal.ErrNotFound +} diff --git a/pkg/service/service.go b/pkg/service/service.go new file mode 100644 index 00000000..9ea399c7 --- /dev/null +++ b/pkg/service/service.go @@ -0,0 +1,66 @@ +package service + +import ( + "context" + "log" + "net/http" + + "github.com/go-chi/chi/v5" + h "github.com/hyphengolang/prelude/http" +) + +type Service interface { + chi.Router + + Context() context.Context + + Log(...any) + Logf(string, ...any) + + Decode(http.ResponseWriter, *http.Request, any) error + Respond(http.ResponseWriter, *http.Request, any, int) + RespondText(w http.ResponseWriter, r *http.Request, status int) + Created(http.ResponseWriter, *http.Request, string) + SetCookie(http.ResponseWriter, *http.Cookie) +} + +type service struct { + ctx context.Context + chi.Router +} + +// Context implements Service +func (s *service) Context() context.Context { + if s.ctx == nil { + return context.Background() + } + return s.ctx +} + +// Created implements Service +func (*service) Created(w http.ResponseWriter, r *http.Request, id string) { h.Created(w, r, id) } + +// Decode implements Service +func (*service) Decode(w http.ResponseWriter, r *http.Request, v any) error { return h.Decode(w, r, v) } + +// Log implements Service +func (*service) Log(v ...any) { log.Println(v...) } + +// Logf implements Service +func (*service) Logf(format string, v ...any) { log.Printf(format, v...) } + +// Respond implements Service +func (*service) Respond(w http.ResponseWriter, r *http.Request, v any, status int) { + h.Respond(w, r, v, status) +} + +func (s *service) RespondText(w http.ResponseWriter, r *http.Request, status int) { + s.Respond(w, r, http.StatusText(status), status) +} + +// SetCookie implements Service +func (*service) SetCookie(w http.ResponseWriter, c *http.Cookie) { http.SetCookie(w, c) } + +func New(ctx context.Context, mux chi.Router) Service { + return &service{ctx, mux} +} From 35cce75a24ce1f3dbeb3372d5d496fac11a10d8a Mon Sep 17 00:00:00 2001 From: aboublef Date: Mon, 12 Dec 2022 18:59:26 +0000 Subject: [PATCH 223/246] auth service passes tests --- service/auth/service.go | 191 +++++++++++++++-------------------- service/auth/service_test.go | 6 +- 2 files changed, 86 insertions(+), 111 deletions(-) diff --git a/service/auth/service.go b/service/auth/service.go index fa6acef0..a091dde8 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -3,21 +3,23 @@ package auth import ( "context" "errors" - "log" + "fmt" "net/http" "time" "github.com/go-chi/chi/v5" "github.com/lestrrat-go/jwx/v2/jwk" - h "github.com/hyphengolang/prelude/http" "github.com/hyphengolang/prelude/types/email" "github.com/hyphengolang/prelude/types/password" // github.com/rog-golang-buddies/rmx/service/internal/auth/auth "github.com/hyphengolang/prelude/types/suid" "github.com/rog-golang-buddies/rmx/internal" - "github.com/rog-golang-buddies/rmx/internal/auth" + "github.com/rog-golang-buddies/rmx/store/user" + + "github.com/rog-golang-buddies/rmx/pkg/auth" + "github.com/rog-golang-buddies/rmx/pkg/service" ) var ( @@ -56,26 +58,16 @@ Refresh token [?] GET /auth/refresh */ type Service struct { - ctx context.Context - - m chi.Router + service.Service - r internal.RWUserRepo + r user.Repo tc internal.TokenClient - - log func(...any) - logf func(string, ...any) - - decode func(http.ResponseWriter, *http.Request, any) error - respond func(http.ResponseWriter, *http.Request, any, int) - created func(http.ResponseWriter, *http.Request, string) - setCookie func(http.ResponseWriter, *http.Cookie) } func (s *Service) routes() { public, private := auth.ES256() - s.m.Route("/api/v1/auth", func(r chi.Router) { + s.Route("/api/v1/auth", func(r chi.Router) { r.Post("/sign-in", s.handleSignIn(private)) r.Delete("/sign-out", s.handleSignOut()) r.Post("/sign-up", s.handleSignUp()) @@ -83,7 +75,7 @@ func (s *Service) routes() { r.Get("/refresh", s.handleRefresh(public, private)) }) - s.m.Route("/api/v1/account", func(r chi.Router) { + s.Route("/api/v1/account", func(r chi.Router) { r.Get("/me", s.handleIdentity(public)) }) } @@ -91,27 +83,23 @@ func (s *Service) routes() { // FIXME this endpoint is broken due to the redis client // We need to try fix this ASAP func (s *Service) handleRefresh(public, private jwk.Key) http.HandlerFunc { - type token struct { - AccessToken string `json:"accessToken"` - } - return func(w http.ResponseWriter, r *http.Request) { // NOTE temp switch away from auth middleware jtk, err := auth.ParseCookie(r, public, cookieName) if err != nil { - s.respond(w, r, err, http.StatusUnauthorized) + s.Respond(w, r, err, http.StatusUnauthorized) return } claim, ok := jtk.PrivateClaims()["email"].(string) if !ok { - s.respondText(w, r, http.StatusInternalServerError) + s.RespondText(w, r, http.StatusInternalServerError) return } u, err := s.r.Select(r.Context(), email.Email(claim)) if err != nil { - s.respond(w, r, err, http.StatusForbidden) + s.Respond(w, r, err, http.StatusForbidden) return } @@ -122,7 +110,7 @@ func (s *Service) handleRefresh(public, private jwk.Key) http.HandlerFunc { // err := s.tc.ValidateRefreshToken(r.Context(), k.Value) // if err != nil { - // s.respond(w, r, err, http.StatusInternalServerError) + // s.Respond(w, r, err, http.StatusInternalServerError) // return // } @@ -130,13 +118,13 @@ func (s *Service) handleRefresh(public, private jwk.Key) http.HandlerFunc { // // this prevents token reuse // err = s.tc.BlackListRefreshToken(r.Context(), k.Value) // if err != nil { - // s.respond(w, r, err, http.StatusInternalServerError) + // s.Respond(w, r, err, http.StatusInternalServerError) // } // cid := j.Subject() // _, ats, rts, err := s.signedTokens(private, claim.String(), suid.SUID(cid)) // if err != nil { - // s.respond(w, r, err, http.StatusInternalServerError) + // s.Respond(w, r, err, http.StatusInternalServerError) // return // } @@ -144,121 +132,80 @@ func (s *Service) handleRefresh(public, private jwk.Key) http.HandlerFunc { _, ats, rts, err := s.signedTokens(private, u) if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) + s.Respond(w, r, err, http.StatusInternalServerError) return } - c := &http.Cookie{ - Path: "/", - Name: cookieName, - Value: string(rts), - HttpOnly: true, - Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - MaxAge: int(auth.RefreshTokenExpiry), - } + c := s.newCookie(w, r, string(rts), auth.RefreshTokenExpiry) - tk := &token{ + tk := &Token{ AccessToken: string(ats), } - s.setCookie(w, c) - s.respond(w, r, tk, http.StatusOK) + s.SetCookie(w, c) + s.Respond(w, r, tk, http.StatusOK) } } func (s *Service) handleIdentity(public jwk.Key) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - // NOTE temp switch away from auth middleware - tk, err := auth.ParseRequest(r, public) + u, err := s.authenticate(w, r, public) if err != nil { - s.respond(w, r, err, http.StatusUnauthorized) + s.Respond(w, r, err, http.StatusUnauthorized) return } - claim, ok := tk.PrivateClaims()["email"].(string) - if !ok { - s.respondText(w, r, http.StatusInternalServerError) - return - } - - u, err := s.r.Select(r.Context(), email.Email(claim)) - if err != nil { - s.respond(w, r, err, http.StatusNotFound) - return - } - - s.respond(w, r, u, http.StatusOK) + s.Respond(w, r, u, http.StatusOK) } } func (s *Service) handleSignIn(privateKey jwk.Key) http.HandlerFunc { - type token struct { - IDToken string `json:"idToken"` - AccessToken string `json:"accessToken"` - } - return func(w http.ResponseWriter, r *http.Request) { var dto User - if err := s.decode(w, r, &dto); err != nil { - s.respond(w, r, err, http.StatusBadRequest) + if err := s.Decode(w, r, &dto); err != nil { + s.Respond(w, r, err, http.StatusBadRequest) return } u, err := s.r.Select(r.Context(), dto.Email) if err != nil { - s.respond(w, r, err, http.StatusNotFound) + s.Respond(w, r, err, http.StatusNotFound) return } if err := u.Password.Compare(dto.Password.String()); err != nil { - s.respond(w, r, err, http.StatusUnauthorized) + s.Respond(w, r, err, http.StatusUnauthorized) return } - // need to replace u.UUID with a client based ID + // NOTE - need to replace u.UUID with a client based ID // this will mean different cookies for multi-device usage u.ID = suid.NewUUID() its, ats, rts, err := s.signedTokens(privateKey, u) if err != nil { - s.respond(w, r, err, http.StatusInternalServerError) + s.Respond(w, r, err, http.StatusInternalServerError) return } - c := &http.Cookie{ - Path: "/", - Name: cookieName, - Value: string(rts), - HttpOnly: true, - Secure: r.TLS != nil, - SameSite: http.SameSiteLaxMode, - MaxAge: int(auth.RefreshTokenExpiry), - } + c := s.newCookie(w, r, string(rts), auth.RefreshTokenExpiry) - tk := &token{ + tk := &Token{ IDToken: string(its), AccessToken: string(ats), } - s.setCookie(w, c) - s.respond(w, r, tk, http.StatusOK) + s.SetCookie(w, c) + s.Respond(w, r, tk, http.StatusOK) } } func (s *Service) handleSignOut() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - c := &http.Cookie{ - Path: "/", - Name: cookieName, - HttpOnly: true, - // Secure: r.TLS != nil, - // SameSite: http.SameSiteLaxMode, - MaxAge: -1, - } + c := s.newCookie(w, r, "", -1) - s.setCookie(w, c) - s.respond(w, r, http.StatusText(http.StatusOK), http.StatusOK) + s.SetCookie(w, c) + s.Respond(w, r, http.StatusText(http.StatusOK), http.StatusOK) } } @@ -266,26 +213,23 @@ func (s *Service) handleSignUp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var u internal.User if err := s.newUser(w, r, &u); err != nil { - s.respond(w, r, err, http.StatusBadRequest) + s.Respond(w, r, err, http.StatusBadRequest) return } if err := s.r.Insert(r.Context(), &u); err != nil { - s.respond(w, r, err, http.StatusInternalServerError) + s.Respond(w, r, err, http.StatusInternalServerError) return } - s.created(w, r, u.ID.ShortUUID().String()) + suid := u.ID.ShortUUID().String() + s.Created(w, r, suid) } } -func (s *Service) respondText(w http.ResponseWriter, r *http.Request, status int) { - s.respond(w, r, http.StatusText(status), status) -} - func (s *Service) newUser(w http.ResponseWriter, r *http.Request, u *internal.User) (err error) { var dto User - if err = s.decode(w, r, &dto); err != nil { + if err = s.Decode(w, r, &dto); err != nil { return } @@ -309,10 +253,42 @@ func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, return suid.ParseString(chi.URLParam(r, "uuid")) } +func (s *Service) newCookie(w http.ResponseWriter, r *http.Request, value string, maxAge time.Duration) *http.Cookie { + c := &http.Cookie{ + Path: "/", + Name: cookieName, + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteLaxMode, + MaxAge: int(maxAge), + Value: string(value), + } + return c +} + +func (s *Service) authenticate(w http.ResponseWriter, r *http.Request, public jwk.Key) (*internal.User, error) { + tk, err := auth.ParseRequest(r, public) + if err != nil { + return nil, err + } + + claim, ok := tk.PrivateClaims()["email"].(string) + if err := fmt.Errorf("email claim does not exist"); !ok { + return nil, err + } + + u, err := s.r.Select(r.Context(), email.MustParse(claim)) + if err != nil { + return nil, err + } + + return u, nil +} + // TODO there is two cid's being used here, need clarification func (s *Service) signedTokens(private jwk.Key, u *internal.User) (its, ats, rts []byte, err error) { o := auth.TokenOption{ - Issuer: "github.com/rog-golang-buddies/rmx", + Issuer: issuer, Subject: u.ID.ShortUUID().String(), // new client ID for tracking user connections // Audience: []string{}, Claims: map[string]any{"email": u.Email}, @@ -339,28 +315,25 @@ func (s *Service) signedTokens(private jwk.Key, u *internal.User) (its, ats, rts return } -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } - -func NewService(ctx context.Context, m chi.Router, r internal.RWUserRepo, tc internal.TokenClient) *Service { - s := &Service{ctx, m, r, tc, log.Println, log.Printf, h.Decode, h.Respond, h.Created, http.SetCookie} +func NewService(ctx context.Context, m chi.Router, r user.Repo, tc internal.TokenClient) *Service { + s := &Service{service.New(ctx, m), r, tc} s.routes() return s } -func (s *Service) Context() context.Context { - if s.ctx != nil { - return s.ctx - } - return context.Background() -} - type User struct { Email email.Email `json:"email"` Username string `json:"username"` Password password.Password `json:"password"` } +type Token struct { + IDToken string `omitempty,json:"idToken"` + AccessToken string `omitempty,json:"accessToken"` +} + const ( + issuer = "github.com/rog-golang-buddies/rmx" cookieName = "RMX_REFRESH_TOKEN" refreshExp = time.Hour * 24 * 7 accessExp = time.Minute * 5 diff --git a/service/auth/service_test.go b/service/auth/service_test.go index e3cfdfda..a8b34c09 100644 --- a/service/auth/service_test.go +++ b/service/auth/service_test.go @@ -11,8 +11,8 @@ import ( "github.com/go-chi/chi/v5" "github.com/hyphengolang/prelude/testing/is" + "github.com/rog-golang-buddies/rmx/pkg/repotest" "github.com/rog-golang-buddies/rmx/store/auth" - "github.com/rog-golang-buddies/rmx/store/user" ) const applicationJson = "application/json" @@ -20,7 +20,9 @@ const applicationJson = "application/json" var s http.Handler func init() { - s = NewService(context.Background(), chi.NewMux(), user.DefaultRepo, auth.DefaultClient) + ctx, mux := context.Background(), chi.NewMux() + + s = NewService(ctx, mux, repotest.NewUserRepo(), auth.DefaultTokenClient) } func TestService(t *testing.T) { From 4664e03e8e8e354aa53abb10ddf429fbcc1249c3 Mon Sep 17 00:00:00 2001 From: aboublef Date: Mon, 12 Dec 2022 18:59:42 +0000 Subject: [PATCH 224/246] jam service passes websocket tests --- service/jam/service.go | 49 ++++++++++++++----------------------- service/jam/service_test.go | 5 ++-- 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/service/jam/service.go b/service/jam/service.go index a69a1a00..0dcf024f 100644 --- a/service/jam/service.go +++ b/service/jam/service.go @@ -3,14 +3,13 @@ package jam import ( "context" "errors" - "log" "net/http" "sync" "time" "github.com/go-chi/chi/v5" + "github.com/rog-golang-buddies/rmx/pkg/service" - h "github.com/hyphengolang/prelude/http" "github.com/hyphengolang/prelude/http/websocket" "github.com/hyphengolang/prelude/types/suid" @@ -34,15 +33,9 @@ import ( // // GET /ws/jam/{uuid} type Service struct { - m chi.Router - // c *ws.Client - - log func(s ...any) - logf func(string, ...any) + service.Service - created func(http.ResponseWriter, *http.Request, string) - respond func(http.ResponseWriter, *http.Request, any, int) - decode func(http.ResponseWriter, *http.Request, interface{}) error + // c *ws.Client } type muxEntry struct { @@ -84,42 +77,41 @@ func (s *Service) routes() { mp: make(map[suid.UUID]muxEntry), } - s.m.Route("/api/v1/jam", func(r chi.Router) { + s.Route("/api/v1/jam", func(r chi.Router) { // r.Get("/", s.handleListRooms()) r.Post("/", s.handleCreateJamRoom(mux)) // r.Get("/{uuid}", s.handleGetRoomData(mux)) }) - s.m.Route("/ws/jam", func(r chi.Router) { + s.Route("/ws/jam", func(r chi.Router) { r.Get("/{uuid}", s.handleP2PComms(mux)) }) } func (s *Service) handleP2PComms(mux *mux) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { // decode uuid from sid, err := s.parseUUID(w, r) if err != nil { - s.respond(w, r, err, http.StatusBadRequest) + s.Respond(w, r, err, http.StatusBadRequest) return } pool, err := mux.Load(sid) if err != nil { - s.respond(w, r, err, http.StatusNotFound) + s.Respond(w, r, err, http.StatusNotFound) return } if err := errors.New("pool has reached max capacity"); pool.IsFull() { - s.respond(w, r, err, http.StatusServiceUnavailable) + s.Respond(w, r, err, http.StatusServiceUnavailable) return } rwc, err := websocket.UpgradeHTTP(w, r) if err != nil { - s.respond(w, r, err, http.StatusUpgradeRequired) + s.Respond(w, r, err, http.StatusUpgradeRequired) return } @@ -134,8 +126,8 @@ func (s *Service) handleCreateJamRoom(mux *mux) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var pl payload - if err := s.decode(w, r, &pl); err != nil { - s.respond(w, r, err, http.StatusBadRequest) + if err := s.Decode(w, r, &pl); err != nil { + s.Respond(w, r, err, http.StatusBadRequest) return } @@ -149,7 +141,7 @@ func (s *Service) handleCreateJamRoom(mux *mux) http.HandlerFunc { e := muxEntry{suid.NewUUID(), pool} mux.Store(e) - s.created(w, r, e.String()) + s.Created(w, r, e.String()) } } @@ -157,7 +149,7 @@ func (s *Service) handleCreateJamRoom(mux *mux) http.HandlerFunc { // return func(w http.ResponseWriter, r *http.Request) { // uid, err := s.parseUUID(w, r) // if err != nil { -// s.respond(w, r, err, http.StatusBadRequest) +// s.Respond(w, r, err, http.StatusBadRequest) // return // } @@ -165,7 +157,7 @@ func (s *Service) handleCreateJamRoom(mux *mux) http.HandlerFunc { // // method as `Get` is nondescriptive // p, err := s.c.Get(uid) // if err != nil { -// s.respond(w, r, err, http.StatusNotFound) +// s.Respond(w, r, err, http.StatusNotFound) // return // } @@ -174,7 +166,7 @@ func (s *Service) handleCreateJamRoom(mux *mux) http.HandlerFunc { // Users: fp.FMap(p.Keys(), func(uid suid.UUID) suid.SUID { return uid.ShortUUID() }), // } -// s.respond(w, r, v, http.StatusOK) +// s.Respond(w, r, v, http.StatusOK) // } // } @@ -197,7 +189,7 @@ func (s *Service) handleCreateJamRoom(mux *mux) http.HandlerFunc { // }), // } -// s.respond(w, r, v, http.StatusOK) +// s.Respond(w, r, v, http.StatusOK) // } // } @@ -220,13 +212,8 @@ var ( ErrSessionExists = errors.New("api: session already exists") ) -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeHTTP(w, r) } - -func NewService(ctx context.Context, r chi.Router) *Service { - s := &Service{ - r, - log.Print, log.Printf, h.Created, h.Respond, h.Decode, - } +func NewService(ctx context.Context, mux chi.Router) *Service { + s := &Service{service.New(ctx, mux)} s.routes() return s } diff --git a/service/jam/service_test.go b/service/jam/service_test.go index a1b48852..881e178f 100644 --- a/service/jam/service_test.go +++ b/service/jam/service_test.go @@ -22,10 +22,11 @@ var stripPrefix = func(s string) string { } func TestService(t *testing.T) { - t.Parallel() is := is.New(t) - h := NewService(context.Background(), chi.NewMux()) + ctx, mux := context.Background(), chi.NewMux() + + h := NewService(ctx, mux) srv := httptest.NewServer(h) t.Cleanup(func() { srv.Close() }) From 9931fa39e4843bf2ff9cc8d92089aeed5451a7f7 Mon Sep 17 00:00:00 2001 From: aboublef Date: Mon, 12 Dec 2022 19:00:23 +0000 Subject: [PATCH 225/246] root service refactor --- service/service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/service.go b/service/service.go index 4767f3fb..6b22092f 100644 --- a/service/service.go +++ b/service/service.go @@ -30,10 +30,10 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeH func New(ctx context.Context, st *store.Store) http.Handler { s := &Service{chi.NewMux(), log.Print, log.Printf, log.Fatal, log.Fatalf} - s.routes() - + // TODO - use mux.Mount instead. But this works auth.NewService(ctx, s.m, st.UserRepo(), st.TokenClient()) jam.NewService(ctx, s.m) + s.routes() return s } From fccb5dfb69a7d83f880dac8881cc93a5a4014f66 Mon Sep 17 00:00:00 2001 From: aboublef Date: Mon, 12 Dec 2022 19:00:38 +0000 Subject: [PATCH 226/246] root store refactor --- store/store.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/store/store.go b/store/store.go index 77bb3071..8d5d2727 100644 --- a/store/store.go +++ b/store/store.go @@ -10,10 +10,10 @@ import ( type Store struct { tc internal.TokenClient - ur *user.Repo + ur user.Repo } -func (s *Store) UserRepo() *user.Repo { +func (s *Store) UserRepo() user.Repo { if s.ur == nil { panic("user repo must not be nil") } From c7a8ced762328c38e551386802ababfb3673c1b6 Mon Sep 17 00:00:00 2001 From: aboublef Date: Mon, 12 Dec 2022 19:00:57 +0000 Subject: [PATCH 227/246] auth token client not touched --- store/auth/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/auth/auth.go b/store/auth/auth.go index fd308791..cfa3fb0f 100644 --- a/store/auth/auth.go +++ b/store/auth/auth.go @@ -9,7 +9,7 @@ import ( "github.com/lestrrat-go/jwx/v2/jwt" ) -var DefaultClient = &client{make(map[string]bool), make(map[string]bool)} +var DefaultTokenClient = &client{make(map[string]bool), make(map[string]bool)} func (c *client) ValidateRefreshToken(ctx context.Context, token string) error { return ErrNotImplemented From 30f36b76c8e2dd169f14a4898be90024b1f54a40 Mon Sep 17 00:00:00 2001 From: aboublef Date: Mon, 12 Dec 2022 19:01:09 +0000 Subject: [PATCH 228/246] user repo refactor --- store/user/repo.go | 177 ++++++++++------------------------------ store/user/repo_test.go | 3 +- 2 files changed, 44 insertions(+), 136 deletions(-) diff --git a/store/user/repo.go b/store/user/repo.go index 6983217d..b69b59fd 100644 --- a/store/user/repo.go +++ b/store/user/repo.go @@ -2,8 +2,6 @@ package user import ( "context" - "log" - "sync" "time" psql "github.com/hyphengolang/prelude/sql/postgres" @@ -17,20 +15,6 @@ import ( "github.com/rog-golang-buddies/rmx/internal" ) -const ( - qryInsert = `insert into "user" (id, email, username, password) values (@id, @email, @username, @password)` - - qrySelectMany = `select id, email, username, password from "user" order by id` - - qrySelectByID = `select id, email, username, password from "user" where id = $1` - qrySelectByEmail = `select id, email, username, password from "user" where email = $1` - qrySelectByUsername = `select id, email, username, password from "user" where username = $1` - - qryDeleteByID = `delete from "user" where id = $1` - qryDeleteByEmail = `delete from "user" where email = $1` - qryDeleteByUsername = `delete from "user" where username = $1` -) - // Definition of our User in the DB layer type User struct { // Primary key. @@ -49,26 +33,49 @@ type User struct { // DeletedAt *time.Time } -type Repo struct { +type Repo interface { + Closer + Writer + Reader +} + +type ReadWriter interface { + Reader + Writer +} + +type Writer interface { + internal.RepoWriter[internal.User] +} + +type Reader interface { + internal.RepoReader[internal.User] +} + +type Closer interface { + internal.RepoCloser +} + +type repo struct { ctx context.Context c *pgxpool.Pool } // This is not really required -func NewRepo(ctx context.Context, conn *pgxpool.Pool) *Repo { - return &Repo{ctx, conn} +func NewRepo(ctx context.Context, conn *pgxpool.Pool) Repo { + return &repo{ctx, conn} } -func (r *Repo) Context() context.Context { +func (r *repo) Context() context.Context { if r.ctx != nil { return r.ctx } return context.Background() } -func (r *Repo) Close() { r.c.Close() } +func (r *repo) Close() { r.c.Close() } -func (r *Repo) Insert(ctx context.Context, u *internal.User) error { +func (r *repo) Insert(ctx context.Context, u *internal.User) error { args := pgx.NamedArgs{ "id": u.ID, "email": u.Email, @@ -79,13 +86,13 @@ func (r *Repo) Insert(ctx context.Context, u *internal.User) error { return psql.Exec(r.c, qryInsert, args) } -func (r *Repo) SelectMany(ctx context.Context) ([]internal.User, error) { +func (r *repo) SelectMany(ctx context.Context) ([]internal.User, error) { return psql.Query(r.c, qrySelectMany, func(r pgx.Rows, u *internal.User) error { return r.Scan(&u.ID, &u.Email, &u.Username, &u.Password) }) } -func (r *Repo) Select(ctx context.Context, key any) (*internal.User, error) { +func (r *repo) Select(ctx context.Context, key any) (*internal.User, error) { var qry string switch key.(type) { case suid.UUID: @@ -101,7 +108,7 @@ func (r *Repo) Select(ctx context.Context, key any) (*internal.User, error) { return &u, psql.QueryRow(r.c, qry, func(r pgx.Row) error { return r.Scan(&u.ID, &u.Username, &u.Email, &u.Password) }, key) } -func (r *Repo) Delete(ctx context.Context, key any) error { +func (r *repo) Delete(ctx context.Context, key any) error { var qry string switch key.(type) { case suid.UUID: @@ -116,116 +123,16 @@ func (r *Repo) Delete(ctx context.Context, key any) error { return psql.Exec(r.c, qry, key) } -// NOTE in-memory implementation not required anymore - -var DefaultRepo = &repo{ - miu: make(map[suid.UUID]*User), - mei: make(map[string]*User), - log: log.Println, - logf: log.Printf, -} - -func (r *repo) Close() {} - -func (r *repo) Delete(ctx context.Context, key any) error { return nil } - -type repo struct { - mu sync.Mutex - miu map[suid.UUID]*User - mei map[string]*User - - log func(v ...any) - logf func(format string, v ...any) -} - -// Remove implements internal.UserRepo -func (r *repo) Remove(ctx context.Context, key any) error { - panic("unimplemented") -} - -func (r *repo) Insert(ctx context.Context, iu *internal.User) error { - r.mu.Lock() - defer r.mu.Unlock() - - if _, found := r.mei[iu.Email.String()]; found { - return internal.ErrAlreadyExists - } - - u := &User{ - ID: iu.ID, - Username: iu.Username, - Email: iu.Email, - Password: iu.Password, - CreatedAt: time.Now(), - } - r.mei[iu.Email.String()], r.miu[iu.ID] = u, u - - return nil -} - -func (r *repo) SelectMany(ctx context.Context) ([]internal.User, error) { - panic("not implemented") -} - -func (r *repo) Select(ctx context.Context, key any) (*internal.User, error) { - switch key := key.(type) { - case suid.UUID: - return r.selectUUID(key) - case email.Email: - return r.selectEmail(key) - case string: - return r.selectUsername(key) - default: - return nil, internal.ErrInvalidType - } -} - -func (r *repo) selectUUID(uid suid.UUID) (*internal.User, error) { - r.mu.Lock() - defer r.mu.Unlock() - - if u, ok := r.miu[uid]; ok { - return &internal.User{ - ID: u.ID, - Username: u.Username, - Email: email.Email(u.Email), - Password: password.PasswordHash(u.Password), - }, nil - } - - return nil, internal.ErrNotFound -} - -func (r *repo) selectUsername(username string) (*internal.User, error) { - r.mu.Lock() - defer r.mu.Unlock() - - for _, u := range r.mei { - if u.Username == username { - return &internal.User{ - ID: u.ID, - Username: u.Username, - Email: email.Email(u.Email), - Password: password.PasswordHash(u.Password), - }, nil - } - } +const ( + qryInsert = `insert into "user" (id, email, username, password) values (@id, @email, @username, @password)` - return nil, internal.ErrNotFound -} + qrySelectMany = `select id, email, username, password from "user" order by id` -func (r *repo) selectEmail(email email.Email) (*internal.User, error) { - r.mu.Lock() - defer r.mu.Unlock() - - if u, ok := r.mei[email.String()]; ok { - return &internal.User{ - ID: u.ID, - Username: u.Username, - Email: u.Email, - Password: password.PasswordHash(u.Password), - }, nil - } + qrySelectByID = `select id, email, username, password from "user" where id = $1` + qrySelectByEmail = `select id, email, username, password from "user" where email = $1` + qrySelectByUsername = `select id, email, username, password from "user" where username = $1` - return nil, internal.ErrNotFound -} + qryDeleteByID = `delete from "user" where id = $1` + qryDeleteByEmail = `delete from "user" where email = $1` + qryDeleteByUsername = `delete from "user" where username = $1` +) diff --git a/store/user/repo_test.go b/store/user/repo_test.go index 3c99daf6..ba745ba9 100644 --- a/store/user/repo_test.go +++ b/store/user/repo_test.go @@ -1,3 +1,4 @@ +// TODO - use the standard sql package instead of pgx package user import ( @@ -16,7 +17,7 @@ import ( /* https://www.covermymeds.com/main/insights/articles/on-update-timestamps-mysql-vs-postgres/ */ -var db internal.RWUserRepo +var db Repo const migration = ` begin; From 2b5213ee4f66e489a8c09997055ac96fed9543ab Mon Sep 17 00:00:00 2001 From: aboublef Date: Mon, 12 Dec 2022 19:01:27 +0000 Subject: [PATCH 229/246] internal package refactor --- internal/internal.go | 16 ---- service/.delete/.go | 207 ------------------------------------------- 2 files changed, 223 deletions(-) delete mode 100644 service/.delete/.go diff --git a/internal/internal.go b/internal/internal.go index 47e87a7e..fd31e53d 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -121,22 +121,6 @@ type TokenWriter interface { BlackListRefreshToken(ctx context.Context, token string) error } -type RWUserRepo interface { - RepoCloser - RepoReader[User] - RepoWriter[User] -} - -type WUserRepo interface { - RepoCloser - RepoWriter[User] -} - -type UserRepo interface { - RepoCloser - RepoReader[User] -} - type RepoReader[Entry any] interface { // Returns an array of users subject to any filter // conditions that are required diff --git a/service/.delete/.go b/service/.delete/.go deleted file mode 100644 index bccb9a2d..00000000 --- a/service/.delete/.go +++ /dev/null @@ -1,207 +0,0 @@ -package auth - -import ( - "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "time" - - "github.com/go-redis/redis/v9" - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/pkg/errors" - "github.com/rog-golang-buddies/rmx/internal/fp" -) - -type Client struct { - rtdb, cidb *redis.Client -} - -var ( - ErrNotImplemented = errors.New("not implemented") - ErrGenerateKey = errors.New("failed to generate new ecdsa key pair") - ErrSignTokens = errors.New("failed to generate signed tokens") - ErrRTValidate = errors.New("failed to validate refresh token") -) - -func NewRedis(addr, password string) *Client { - rtdb := redis.Options{Addr: addr, Password: password, DB: 0} - cidb := redis.Options{Addr: addr, Password: password, DB: 1} - - c := &Client{redis.NewClient(&rtdb), redis.NewClient(&cidb)} - return c -} - -const ( - defaultAddr = "localhost:6379" - defaultPassword = "" -) - -var DefaultClient = &Client{ - rtdb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 0}), - cidb: redis.NewClient(&redis.Options{Addr: defaultAddr, Password: defaultPassword, DB: 1}), -} - -func (c *Client) ValidateRefreshToken(ctx context.Context, token string) error { - tc, err := ParseRefreshTokenClaims(token) - if err != nil { - return err - } - email, ok := tc.PrivateClaims()["email"].(string) - if !ok { - return ErrRTValidate - } - - cid := tc.Subject() - if err := c.ValidateClientID(ctx, cid); err != nil { - return err - } - - if _, err := c.rtdb.Get(ctx, token).Result(); err != nil { - switch err { - case redis.Nil: - return nil - default: - return err - } - } - - err = c.RevokeClientID(ctx, cid, email) - if err != nil { - return err - } - - return ErrRTValidate -} - -func (c *Client) RevokeClientID(ctx context.Context, cid, email string) error { - _, err := c.cidb.Set(ctx, cid, email, RefreshTokenExpiry).Result() - if err != nil { - return err - } - return nil -} - -func (c *Client) RevokeRefreshToken(ctx context.Context, token string) error { - _, err := c.rtdb.Set(ctx, token, nil, RefreshTokenExpiry).Result() - return err -} - -func (c *Client) ValidateClientID(ctx context.Context, cid string) error { - // check if a key with client id exists - // if the key exists it means that the client id is revoked and token should be denied - // we don't need the email value here - _, err := c.cidb.Get(ctx, cid).Result() - if err != nil { - switch err { - case redis.Nil: - return nil - default: - return ErrRTValidate - } - } - - return ErrRTValidate -} - -/* -// would like to find an alternative to using `os` package -func LoadPEM(path string) (private, public jwk.Key, err error) { - buf, err := os.ReadFile(path) - if err != nil { - return - } - - return GenerateKeys(string(buf)) -} -*/ - -func GenerateKeys() (jwk.Key, jwk.Key, error) { - private, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, nil, err - } - - key, err := jwk.FromRaw(private) - if err != nil { - return nil, nil, err - } - - _, ok := key.(jwk.ECDSAPrivateKey) - if !ok { - return nil, nil, ErrGenerateKey - } - - pub, err := key.PublicKey() - if err != nil { - return nil, nil, err - } - - return key, pub, nil -} - -func SignToken(key *jwk.Key, opt *TokenOption) ([]byte, error) { - var t time.Time - if opt.IssuedAt.IsZero() { - t = time.Now().UTC() - } else { - t = opt.IssuedAt - } - - token, err := jwt.NewBuilder(). - Issuer(opt.Issuer). - Audience(opt.Audience). - Subject(opt.Subject). - IssuedAt(t). - Expiration(t.Add(opt.Expiration)). - Build() - if err != nil { - return nil, ErrSignTokens - } - - for _, c := range opt.Claims { - if !c.HasValue() { - return nil, fp.ErrTuple - } - - err := token.Set(c[0], c[1]) - if err != nil { - return nil, ErrSignTokens - } - } - - return jwt.Sign(token, jwt.WithKey(jwa.ES256, key)) -} - -func ParseRefreshTokenClaims(token string) (jwt.Token, error) { return jwt.Parse([]byte(token)) } - -func ParseRefreshTokenWithValidate(key *jwk.Key, token string) (jwt.Token, error) { - payload, err := jwt.Parse([]byte(token), - jwt.WithKey(jwa.ES256, key), - jwt.WithValidate(true)) - if err != nil { - return nil, err - } - - return payload, nil -} - -type TokenOption struct { - Issuer string - Audience []string - Subject string - Claims []fp.Tuple - IssuedAt time.Time - Expiration time.Duration -} - -type authCtxKey string - -const ( - RefreshTokenCookieName = "RMX_REFRESH_TOKEN" - RefreshTokenExpiry = time.Hour * 24 * 7 - AccessTokenExpiry = time.Minute * 5 - EmailKey = authCtxKey("rmx-email") -) From 8ab2d4e2b71be2bc893910a0ffbbc1cbbbb840fb Mon Sep 17 00:00:00 2001 From: aboublef Date: Mon, 12 Dec 2022 19:41:47 +0000 Subject: [PATCH 230/246] auth refactoring --- service/auth/service.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/service/auth/service.go b/service/auth/service.go index a091dde8..ec782127 100644 --- a/service/auth/service.go +++ b/service/auth/service.go @@ -295,19 +295,19 @@ func (s *Service) signedTokens(private jwk.Key, u *internal.User) (its, ats, rts } // its - o.Expiration = time.Hour * 10 + o.Expiration = idTokenExp if its, err = auth.Sign(private, &o); err != nil { return } // ats - o.Expiration = time.Minute * 5 + o.Expiration = accessTokenExp if ats, err = auth.Sign(private, &o); err != nil { return } // rts - o.Expiration = time.Hour * 24 * 7 + o.Expiration = refreshTokenExp if rts, err = auth.Sign(private, &o); err != nil { return } @@ -333,8 +333,9 @@ type Token struct { } const ( - issuer = "github.com/rog-golang-buddies/rmx" - cookieName = "RMX_REFRESH_TOKEN" - refreshExp = time.Hour * 24 * 7 - accessExp = time.Minute * 5 + issuer = "github.com/rog-golang-buddies/rmx" + cookieName = "RMX_REFRESH_TOKEN" + idTokenExp = time.Hour * 10 + refreshTokenExp = time.Hour * 24 * 7 + accessTokenExp = time.Minute * 5 ) From 9722785be1f67113cf0338069db4100b22e10d4c Mon Sep 17 00:00:00 2001 From: pmoieni Date: Mon, 26 Dec 2022 22:05:41 +0330 Subject: [PATCH 231/246] new websocket implementation (in development) --- go.mod | 5 +- go.sum | 2 - internal/websocket/websocket.go | 283 +++++++++++++++++++++++++++ internal/websocket/websocket_test.go | 9 + service/jam/v2/service.go | 181 +++++++++++++++++ service/jam/v2/service_test.go | 65 ++++++ 6 files changed, 540 insertions(+), 5 deletions(-) create mode 100644 internal/websocket/websocket.go create mode 100644 internal/websocket/websocket_test.go create mode 100644 service/jam/v2/service.go create mode 100644 service/jam/v2/service_test.go diff --git a/go.mod b/go.mod index 7ee32ec3..6d98d6ff 100644 --- a/go.mod +++ b/go.mod @@ -9,15 +9,12 @@ require ( github.com/go-chi/chi v1.5.4 github.com/go-chi/chi/v5 v5.0.7 github.com/gobwas/ws v1.1.0 - github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 github.com/hyphengolang/prelude v0.1.3 - github.com/lithammer/shortuuid/v4 v4.0.0 github.com/manifoldco/promptui v0.9.0 github.com/pkg/errors v0.9.1 github.com/rs/cors v1.8.2 github.com/urfave/cli/v2 v2.16.3 - golang.org/x/exp v0.0.0-20220921164117-439092de6870 golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 golang.org/x/term v0.0.0-20220919170432-7a66f970e087 ) @@ -30,6 +27,7 @@ require ( github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/goccy/go-json v0.9.11 // indirect + github.com/google/uuid v1.3.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect github.com/jackc/puddle/v2 v2.0.0 // indirect @@ -39,6 +37,7 @@ require ( github.com/lestrrat-go/httprc v1.0.4 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.0 // indirect + github.com/lithammer/shortuuid/v4 v4.0.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect ) diff --git a/go.sum b/go.sum index 06fab041..50a7c7c7 100644 --- a/go.sum +++ b/go.sum @@ -137,8 +137,6 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20220921164117-439092de6870 h1:j8b6j9gzSigH28O5SjSpQSSh9lFd6f5D/q0aHjNTulc= -golang.org/x/exp v0.0.0-20220921164117-439092de6870/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc= diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go new file mode 100644 index 00000000..79fb9084 --- /dev/null +++ b/internal/websocket/websocket.go @@ -0,0 +1,283 @@ +package websocket + +import ( + "context" + "encoding/json" + "errors" + "io" + "sync" + "time" + + "github.com/gobwas/ws/wsutil" + "github.com/hyphengolang/prelude/types/suid" + "golang.org/x/sync/errgroup" +) + +type WSMsgTyp int + +const ( + Text WSMsgTyp = iota + 1 + JSON + Leave +) + +// Broker contains the list of the Subscribers +type Broker[SI, CI any] struct { + lock sync.RWMutex + // list of Subscribers + ss map[suid.UUID]*Subscriber[SI, CI] + + // Maximum Capacity Subscribers allowed + Capacity uint + Context context.Context +} + +func NewBroker[SI, CI any](cap uint, ctx context.Context) *Broker[SI, CI] { + return &Broker[SI, CI]{ + ss: make(map[suid.UUID]*Subscriber[SI, CI]), + Capacity: cap, + Context: ctx, + } +} + +// Adds a new Subscriber to the list +func (b *Broker[SI, CI]) Subscribe(s *Subscriber[SI, CI]) error { + b.lock.Lock() + defer b.lock.Unlock() + b.ss[s.sid] = s + + return nil +} + +func (b *Broker[SI, CI]) GetSubscriber(sid suid.UUID) (*Subscriber[SI, CI], error) { + b.lock.Lock() + defer b.lock.Unlock() + s, ok := b.ss[sid] + + if !ok { + return nil, errors.New("Subscriber not found") + } + + return s, nil +} + +// Subscriber contains the list of the connections +type Subscriber[SI, CI any] struct { + // unique id for the Subscriber + sid suid.UUID + lock sync.RWMutex + // list of Connections + cs map[suid.UUID]*Conn[CI] + // Subscriber status + online bool + // Input/Output channel for new messages + io chan *message + // Maximum Capacity clients allowed + Capacity uint + // Maximum message size allowed from peer. + ReadBufferSize int64 + // Time allowed to read the next pong message from the peer. + ReadTimeout time.Duration + // Time allowed to write a message to the peer. + WriteTimeout time.Duration + // Info binds its value(like a Jam session) to the subscriber + Info *SI + Context context.Context +} + +func (b *Broker[SI, CI]) NewSubscriber( + cap uint, + rs int64, + rt time.Duration, + wt time.Duration, + i *SI, +) *Subscriber[SI, CI] { + return &Subscriber[SI, CI]{ + sid: suid.NewUUID(), + cs: make(map[suid.UUID]*Conn[CI]), + io: make(chan *message), + Capacity: cap, + ReadBufferSize: rs, + ReadTimeout: rt, + WriteTimeout: wt, + Info: i, + Context: b.Context, + } +} + +func (s *Subscriber[SI, CI]) Listen() error { + return s.listen() +} + +func (s *Subscriber[SI, CI]) Connect(c *Conn[CI]) error { + return s.connect(c) +} + +func (s *Subscriber[SI, CI]) Disconnect(c *Conn[CI]) error { + return s.disconnect(c) +} + +func (s *Subscriber[SI, CI]) IsFull() bool { + if s.Capacity == 0 { + return false + } + + return len(s.cs) >= int(s.Capacity) +} + +func (s *Subscriber[SI, CI]) GetID() suid.UUID { + return s.sid +} + +// Starts listening on the io channel which Connections send their messages to +func (s *Subscriber[SI, CI]) listen() error { + s.online = true + + for m := range s.io { + s.lock.RLock() + cs := s.cs + s.lock.Unlock() + + for _, c := range cs { + if err := s.write(c, m.marshall()); err != nil { + return err + } + } + } + + return nil +} + +// Connects the given Connection to the Subscriber and adds it to the list of its Connections +func (s *Subscriber[SI, CI]) connect(c *Conn[CI]) error { + s.lock.Lock() + defer s.lock.Unlock() + + // check if the Subscriber is listening + if s.online { + // add the connection to the list + s.cs[c.sid] = c + + // create an error group to catch goroutine errors + g, _ := errgroup.WithContext(s.Context) + g.Go(func() error { + return s.read(c) + }) + + // wait for errors + err := g.Wait() + if err != nil { + if err := s.disconnect(c); err != nil { + return err + } + + return err + } + } + + return nil +} + +// Closes the given Connection and removes it from the Connections list +func (s *Subscriber[SI, CI]) disconnect(c *Conn[CI]) error { + // close websocket connection + if err := c.rwc.Close(); err != nil { + return err + } + + // remove connection from the list + delete(s.cs, c.sid) + return nil +} + +// Starts reading from the given Connection +func (s *Subscriber[SI, CI]) read(c *Conn[CI]) error { + c.lock.RLock() + defer c.lock.RUnlock() + + // read binary from connection + b, err := wsutil.ReadClientBinary(c.rwc) + if err != nil { + return err + } + + var m *message + m.parse(b) + + switch m.typ { + case Leave: + return s.disconnect(c) + default: + s.io <- m + } + + return nil +} + +// Writes raw bytes to the Connection +func (s *Subscriber[SI, CI]) write(c *Conn[CI], b []byte) error { + c.lock.RLock() + defer c.lock.RUnlock() + + return wsutil.WriteServerBinary(c.rwc, b) +} + +// A Web-Socket Connection +type Conn[CI any] struct { + sid suid.UUID + rwc io.ReadWriteCloser + lock sync.RWMutex + + Info *CI +} + +func NewConn[CI any](rwc io.ReadWriteCloser, info *CI) *Conn[CI] { + return &Conn[CI]{ + sid: suid.NewUUID(), + rwc: rwc, + Info: info, + } +} + +// type for parsing bytes into messages +type message struct { + typ WSMsgTyp + data []byte +} + +// Parses the bytes into the message type +func (m *message) parse(b []byte) { + // the first byte represents the data type (Text, JSON, Leave) + m.typ = WSMsgTyp(b[0]) + // and others represent the data itself + m.data = b[1:] +} + +func (m *message) marshall() []byte { + return append([]byte{byte(m.typ)}, m.data...) +} + +// Converts the given bytes to string +func (m *message) readText() (string, error) { + return string(m.data), nil +} + +// Converts the given bytes to JSON +func (m *message) readJSON() (any, error) { + var v any + if err := json.Unmarshal(m.data, v); err != nil { + return nil, err + } + + return v, nil +} + +type Reader interface { + ReadText() (string, error) + ReadJSON() (interface{}, error) +} + +type Writer interface { + WriteText(s string) + WriteJSON(i any) error +} diff --git a/internal/websocket/websocket_test.go b/internal/websocket/websocket_test.go new file mode 100644 index 00000000..43b4db4c --- /dev/null +++ b/internal/websocket/websocket_test.go @@ -0,0 +1,9 @@ +package websocket + +// func TestWebsocket(t *testing.T) { +// is := is.New(t) +// mux := chi.NewMux() +// srv := httptest.NewServer(mux) + +// t.Run("Create a new jam session") +// } diff --git a/service/jam/v2/service.go b/service/jam/v2/service.go new file mode 100644 index 00000000..7cf2d592 --- /dev/null +++ b/service/jam/v2/service.go @@ -0,0 +1,181 @@ +package v2 + +import ( + "context" + "errors" + "log" + "net/http" + "strings" + "time" + + "github.com/go-chi/chi/v5" + "github.com/gobwas/ws" + "github.com/hyphengolang/prelude/types/suid" + "github.com/rog-golang-buddies/rmx/internal/websocket" + "github.com/rog-golang-buddies/rmx/pkg/service" +) + +// Jam Service Endpoints +// +// Create a new jam session. +// +// POST /api/v1/jam +// +// List all jam sessions metadata. +// +// GET /api/v1/jam +// +// Get a jam sessions metadata. +// +// GET /api/v1/jam/{uuid} +// +// Connect to jam session. +// +// GET /ws/jam/{uuid} + +type Service struct { + service.Service +} + +func NewService(ctx context.Context, mux chi.Router) *Service { + s := &Service{service.New(ctx, mux)} + s.routes() + return s +} + +const ( + defaultTimeout = time.Second * 10 +) + +type User struct { + id suid.UUID + Username string `json:"username"` +} + +func (u *User) fillDefaults() { + u.id = suid.NewUUID() + if strings.TrimSpace(u.Username) == "" { + u.Username = u.id.String() + } +} + +type Jam struct { + id suid.UUID + owner *User + Name string `json:"name, omitempty"` + Capacity uint `json:"capacity, omitempty"` + BPM uint `json:"bpm, omitempty"` +} + +func (j *Jam) fillDefaults() { + j.id = suid.NewUUID() + if j.owner == nil { + j.owner = &User{j.id, j.Name} + } + if strings.TrimSpace(j.Name) == "" { + j.Name = j.id.ShortUUID().String() + } + if j.Capacity == 0 { + j.Capacity = 10 + } + if j.BPM == 0 { + j.BPM = 80 + } +} + +func (s *Service) handleCreateJamRoom(b *websocket.Broker[Jam, User]) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var j Jam + if err := s.Decode(w, r, &j); err != nil { + s.Respond(w, r, err, http.StatusBadRequest) + return + } + + // fill out empty fields with default value. + j.fillDefaults() + + // create a new Subscriber + sub := b.NewSubscriber(j.Capacity, 512, defaultTimeout, defaultTimeout, &j) + + // add the Subscriber to Broker + if err := b.Subscribe(sub); err != nil { + s.Respond(w, r, err, http.StatusInternalServerError) + return + } + + if err := sub.Listen(); err != nil { + s.Respond(w, r, err, http.StatusInternalServerError) + return + } + + s.Created(w, r, sub.GetID().ShortUUID().String()) + } +} + +func (s *Service) handleGetRoomData(b *websocket.Broker[Jam, User]) http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + + } +} + +func (s *Service) handleListRooms(b *websocket.Broker[Jam, User]) http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + + } +} + +func (s *Service) handleP2PComms(b *websocket.Broker[Jam, User]) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // decode uuid from + sid, err := s.parseUUID(w, r) + if err != nil { + s.Respond(w, r, sid, http.StatusBadRequest) + return + } + + sub, err := b.GetSubscriber(sid) + if err != nil { + s.Respond(w, r, err, http.StatusNotFound) + return + } + + if err := errors.New("subscriber has reached max capacity"); sub.IsFull() { + s.Respond(w, r, err, http.StatusServiceUnavailable) + return + } + + rwc, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + s.Respond(w, r, err, http.StatusUpgradeRequired) + return + } + + var u User + u.fillDefaults() + + conn := websocket.NewConn(rwc, &u) + err = sub.Connect(conn) + log.Fatal(err) + } +} + +func (s *Service) routes() { + broker := websocket.NewBroker[Jam, User](10, context.Background()) + + s.Route("/api/v1/jam", func(r chi.Router) { + r.Get("/", s.handleListRooms(broker)) + r.Get("/{uuid}", s.handleGetRoomData(broker)) + r.Post("/", s.handleCreateJamRoom(broker)) + }) + + s.Route("/ws/jam", func(r chi.Router) { + r.Get("/{uuid}", s.handleP2PComms(broker)) + }) + +} + +func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { + return suid.ParseString(chi.URLParam(r, "uuid")) +} diff --git a/service/jam/v2/service_test.go b/service/jam/v2/service_test.go new file mode 100644 index 00000000..242cf4a9 --- /dev/null +++ b/service/jam/v2/service_test.go @@ -0,0 +1,65 @@ +package v2 + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" + "github.com/hyphengolang/prelude/testing/is" +) + +var resource = func(s string) string { + return s[strings.LastIndex(s, "/")+1:] +} + +var stripPrefix = func(s string) string { + return "ws" + strings.TrimPrefix(s, "http") +} + +func TestService(t *testing.T) { + is := is.New(t) + ctx, mux := context.Background(), chi.NewMux() + h := NewService(ctx, mux) + srv := httptest.NewServer(h) + + var firstJam string + t.Run("Create a new Jam room", func(t *testing.T) { + payload := `{ + "name": "John Doe", + "capacity": 5, + "bpm": 100 + }` + + res, _ := srv.Client().Post(srv.URL+"/api/v1/jam", "application/json", strings.NewReader(payload)) + is.Equal(res.StatusCode, http.StatusCreated) // created a new resource + + loc, err := res.Location() + is.NoErr(err) // retrieve location + + firstJam = resource(loc.Path) + t.Log(firstJam) + }) + + t.Run(`Connect to Jam room with id: `+firstJam, func(t *testing.T) { + c1, _, _, err := ws.DefaultDialer.Dial(ctx, stripPrefix(srv.URL+"/ws/jam/"+firstJam)) + is.NoErr(err) // found first jam Session + + t.Cleanup(func() { c1.Close() }) + + text := "Hello, World!" + + err = wsutil.WriteClientBinary(c1, []byte(text)) + is.NoErr(err) // write to pool + + data, err := wsutil.ReadServerBinary(c1) + is.NoErr(err) // read from pool + + is.Equal(string(data), text) + }) + +} From 23f6b4c80ffe20b3c0eaef58c2fe5d2e7793cd70 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Wed, 28 Dec 2022 10:51:42 +0330 Subject: [PATCH 232/246] fix JSON tags --- internal/websocket/broker.go | 49 ++++++ internal/websocket/conn.go | 17 ++ internal/websocket/message.go | 44 +++++ internal/websocket/subscriber.go | 181 ++++++++++++++++++++ internal/websocket/websocket.go | 272 ------------------------------- service/jam/v2/service.go | 8 +- 6 files changed, 295 insertions(+), 276 deletions(-) create mode 100644 internal/websocket/broker.go create mode 100644 internal/websocket/conn.go create mode 100644 internal/websocket/message.go create mode 100644 internal/websocket/subscriber.go diff --git a/internal/websocket/broker.go b/internal/websocket/broker.go new file mode 100644 index 00000000..68787862 --- /dev/null +++ b/internal/websocket/broker.go @@ -0,0 +1,49 @@ +package websocket + +import ( + "context" + "errors" + "sync" + + "github.com/hyphengolang/prelude/types/suid" +) + +// Broker contains the list of the Subscribers +type Broker[SI, CI any] struct { + lock sync.RWMutex + // list of Subscribers + ss map[suid.UUID]*Subscriber[SI, CI] + + // Maximum Capacity Subscribers allowed + Capacity uint + Context context.Context +} + +func NewBroker[SI, CI any](cap uint, ctx context.Context) *Broker[SI, CI] { + return &Broker[SI, CI]{ + ss: make(map[suid.UUID]*Subscriber[SI, CI]), + Capacity: cap, + Context: ctx, + } +} + +// Adds a new Subscriber to the list +func (b *Broker[SI, CI]) Subscribe(s *Subscriber[SI, CI]) error { + b.lock.Lock() + defer b.lock.Unlock() + b.ss[s.sid] = s + + return nil +} + +func (b *Broker[SI, CI]) GetSubscriber(sid suid.UUID) (*Subscriber[SI, CI], error) { + b.lock.Lock() + defer b.lock.Unlock() + s, ok := b.ss[sid] + + if !ok { + return nil, errors.New("Subscriber not found") + } + + return s, nil +} diff --git a/internal/websocket/conn.go b/internal/websocket/conn.go new file mode 100644 index 00000000..5bf7878b --- /dev/null +++ b/internal/websocket/conn.go @@ -0,0 +1,17 @@ +package websocket + +import ( + "io" + "sync" + + "github.com/hyphengolang/prelude/types/suid" +) + +// A Web-Socket Connection +type Conn[CI any] struct { + sid suid.UUID + rwc io.ReadWriteCloser + lock sync.RWMutex + + Info *CI +} diff --git a/internal/websocket/message.go b/internal/websocket/message.go new file mode 100644 index 00000000..0088b22e --- /dev/null +++ b/internal/websocket/message.go @@ -0,0 +1,44 @@ +package websocket + +import "encoding/json" + +type WSMsgTyp int + +const ( + Text WSMsgTyp = iota + 1 + JSON + Leave +) + +// type for parsing bytes into messages +type message struct { + typ WSMsgTyp + data []byte +} + +// Parses the bytes into the message type +func (m *message) parse(b []byte) { + // the first byte represents the data type (Text, JSON, Leave) + m.typ = WSMsgTyp(b[0]) + // and others represent the data itself + m.data = b[1:] +} + +func (m *message) marshall() []byte { + return append([]byte{byte(m.typ)}, m.data...) +} + +// Converts the given bytes to string +func (m *message) readText() (string, error) { + return string(m.data), nil +} + +// Converts the given bytes to JSON +func (m *message) readJSON() (any, error) { + var v any + if err := json.Unmarshal(m.data, v); err != nil { + return nil, err + } + + return v, nil +} diff --git a/internal/websocket/subscriber.go b/internal/websocket/subscriber.go new file mode 100644 index 00000000..7487cc60 --- /dev/null +++ b/internal/websocket/subscriber.go @@ -0,0 +1,181 @@ +package websocket + +import ( + "context" + "io" + "sync" + "time" + + "github.com/gobwas/ws/wsutil" + "github.com/hyphengolang/prelude/types/suid" + "golang.org/x/sync/errgroup" +) + +// Subscriber contains the list of the connections +type Subscriber[SI, CI any] struct { + // unique id for the Subscriber + sid suid.UUID + lock sync.RWMutex + // list of Connections + cs map[suid.UUID]*Conn[CI] + // Subscriber status + online bool + // Input/Output channel for new messages + io chan *message + // Maximum Capacity clients allowed + Capacity uint + // Maximum message size allowed from peer. + ReadBufferSize int64 + // Time allowed to read the next pong message from the peer. + ReadTimeout time.Duration + // Time allowed to write a message to the peer. + WriteTimeout time.Duration + // Info binds its value(like a Jam session) to the subscriber + Info *SI + Context context.Context +} + +func (b *Broker[SI, CI]) NewSubscriber( + cap uint, + rs int64, + rt time.Duration, + wt time.Duration, + i *SI, +) *Subscriber[SI, CI] { + return &Subscriber[SI, CI]{ + sid: suid.NewUUID(), + cs: make(map[suid.UUID]*Conn[CI]), + io: make(chan *message), + Capacity: cap, + ReadBufferSize: rs, + ReadTimeout: rt, + WriteTimeout: wt, + Info: i, + Context: b.Context, + } +} + +func (s *Subscriber[SI, CI]) Listen() error { + return s.listen() +} + +func (s *Subscriber[SI, CI]) Connect(c *Conn[CI]) error { + return s.connect(c) +} + +func (s *Subscriber[SI, CI]) Disconnect(c *Conn[CI]) error { + return s.disconnect(c) +} + +func (s *Subscriber[SI, CI]) IsFull() bool { + if s.Capacity == 0 { + return false + } + + return len(s.cs) >= int(s.Capacity) +} + +func (s *Subscriber[SI, CI]) NewConn(rwc io.ReadWriteCloser, info *CI) *Conn[CI] { + return &Conn[CI]{ + sid: suid.NewUUID(), + rwc: rwc, + Info: info, + } +} + +func (s *Subscriber[SI, CI]) GetID() suid.UUID { + return s.sid +} + +// Starts listening on the io channel which Connections send their messages to +func (s *Subscriber[SI, CI]) listen() error { + s.online = true + + for m := range s.io { + s.lock.RLock() + cs := s.cs + s.lock.Unlock() + + for _, c := range cs { + if err := s.write(c, m.marshall()); err != nil { + return err + } + } + } + + return nil +} + +// Connects the given Connection to the Subscriber and adds it to the list of its Connections +func (s *Subscriber[SI, CI]) connect(c *Conn[CI]) error { + s.lock.Lock() + defer s.lock.Unlock() + + // check if the Subscriber is listening + if s.online { + // add the connection to the list + s.cs[c.sid] = c + + // create an error group to catch goroutine errors + g, _ := errgroup.WithContext(s.Context) + g.Go(func() error { + return s.read(c) + }) + + // wait for errors + err := g.Wait() + if err != nil { + if err := s.disconnect(c); err != nil { + return err + } + + return err + } + } + + return nil +} + +// Closes the given Connection and removes it from the Connections list +func (s *Subscriber[SI, CI]) disconnect(c *Conn[CI]) error { + // close websocket connection + if err := c.rwc.Close(); err != nil { + return err + } + + // remove connection from the list + delete(s.cs, c.sid) + return nil +} + +// Starts reading from the given Connection +func (s *Subscriber[SI, CI]) read(c *Conn[CI]) error { + c.lock.RLock() + defer c.lock.RUnlock() + + // read binary from connection + b, err := wsutil.ReadClientBinary(c.rwc) + if err != nil { + return err + } + + var m *message + m.parse(b) + + switch m.typ { + case Leave: + return s.disconnect(c) + default: + s.io <- m + } + + return nil +} + +// Writes raw bytes to the Connection +func (s *Subscriber[SI, CI]) write(c *Conn[CI], b []byte) error { + c.lock.RLock() + defer c.lock.RUnlock() + + return wsutil.WriteServerBinary(c.rwc, b) +} diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 79fb9084..1c792001 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -1,277 +1,5 @@ package websocket -import ( - "context" - "encoding/json" - "errors" - "io" - "sync" - "time" - - "github.com/gobwas/ws/wsutil" - "github.com/hyphengolang/prelude/types/suid" - "golang.org/x/sync/errgroup" -) - -type WSMsgTyp int - -const ( - Text WSMsgTyp = iota + 1 - JSON - Leave -) - -// Broker contains the list of the Subscribers -type Broker[SI, CI any] struct { - lock sync.RWMutex - // list of Subscribers - ss map[suid.UUID]*Subscriber[SI, CI] - - // Maximum Capacity Subscribers allowed - Capacity uint - Context context.Context -} - -func NewBroker[SI, CI any](cap uint, ctx context.Context) *Broker[SI, CI] { - return &Broker[SI, CI]{ - ss: make(map[suid.UUID]*Subscriber[SI, CI]), - Capacity: cap, - Context: ctx, - } -} - -// Adds a new Subscriber to the list -func (b *Broker[SI, CI]) Subscribe(s *Subscriber[SI, CI]) error { - b.lock.Lock() - defer b.lock.Unlock() - b.ss[s.sid] = s - - return nil -} - -func (b *Broker[SI, CI]) GetSubscriber(sid suid.UUID) (*Subscriber[SI, CI], error) { - b.lock.Lock() - defer b.lock.Unlock() - s, ok := b.ss[sid] - - if !ok { - return nil, errors.New("Subscriber not found") - } - - return s, nil -} - -// Subscriber contains the list of the connections -type Subscriber[SI, CI any] struct { - // unique id for the Subscriber - sid suid.UUID - lock sync.RWMutex - // list of Connections - cs map[suid.UUID]*Conn[CI] - // Subscriber status - online bool - // Input/Output channel for new messages - io chan *message - // Maximum Capacity clients allowed - Capacity uint - // Maximum message size allowed from peer. - ReadBufferSize int64 - // Time allowed to read the next pong message from the peer. - ReadTimeout time.Duration - // Time allowed to write a message to the peer. - WriteTimeout time.Duration - // Info binds its value(like a Jam session) to the subscriber - Info *SI - Context context.Context -} - -func (b *Broker[SI, CI]) NewSubscriber( - cap uint, - rs int64, - rt time.Duration, - wt time.Duration, - i *SI, -) *Subscriber[SI, CI] { - return &Subscriber[SI, CI]{ - sid: suid.NewUUID(), - cs: make(map[suid.UUID]*Conn[CI]), - io: make(chan *message), - Capacity: cap, - ReadBufferSize: rs, - ReadTimeout: rt, - WriteTimeout: wt, - Info: i, - Context: b.Context, - } -} - -func (s *Subscriber[SI, CI]) Listen() error { - return s.listen() -} - -func (s *Subscriber[SI, CI]) Connect(c *Conn[CI]) error { - return s.connect(c) -} - -func (s *Subscriber[SI, CI]) Disconnect(c *Conn[CI]) error { - return s.disconnect(c) -} - -func (s *Subscriber[SI, CI]) IsFull() bool { - if s.Capacity == 0 { - return false - } - - return len(s.cs) >= int(s.Capacity) -} - -func (s *Subscriber[SI, CI]) GetID() suid.UUID { - return s.sid -} - -// Starts listening on the io channel which Connections send their messages to -func (s *Subscriber[SI, CI]) listen() error { - s.online = true - - for m := range s.io { - s.lock.RLock() - cs := s.cs - s.lock.Unlock() - - for _, c := range cs { - if err := s.write(c, m.marshall()); err != nil { - return err - } - } - } - - return nil -} - -// Connects the given Connection to the Subscriber and adds it to the list of its Connections -func (s *Subscriber[SI, CI]) connect(c *Conn[CI]) error { - s.lock.Lock() - defer s.lock.Unlock() - - // check if the Subscriber is listening - if s.online { - // add the connection to the list - s.cs[c.sid] = c - - // create an error group to catch goroutine errors - g, _ := errgroup.WithContext(s.Context) - g.Go(func() error { - return s.read(c) - }) - - // wait for errors - err := g.Wait() - if err != nil { - if err := s.disconnect(c); err != nil { - return err - } - - return err - } - } - - return nil -} - -// Closes the given Connection and removes it from the Connections list -func (s *Subscriber[SI, CI]) disconnect(c *Conn[CI]) error { - // close websocket connection - if err := c.rwc.Close(); err != nil { - return err - } - - // remove connection from the list - delete(s.cs, c.sid) - return nil -} - -// Starts reading from the given Connection -func (s *Subscriber[SI, CI]) read(c *Conn[CI]) error { - c.lock.RLock() - defer c.lock.RUnlock() - - // read binary from connection - b, err := wsutil.ReadClientBinary(c.rwc) - if err != nil { - return err - } - - var m *message - m.parse(b) - - switch m.typ { - case Leave: - return s.disconnect(c) - default: - s.io <- m - } - - return nil -} - -// Writes raw bytes to the Connection -func (s *Subscriber[SI, CI]) write(c *Conn[CI], b []byte) error { - c.lock.RLock() - defer c.lock.RUnlock() - - return wsutil.WriteServerBinary(c.rwc, b) -} - -// A Web-Socket Connection -type Conn[CI any] struct { - sid suid.UUID - rwc io.ReadWriteCloser - lock sync.RWMutex - - Info *CI -} - -func NewConn[CI any](rwc io.ReadWriteCloser, info *CI) *Conn[CI] { - return &Conn[CI]{ - sid: suid.NewUUID(), - rwc: rwc, - Info: info, - } -} - -// type for parsing bytes into messages -type message struct { - typ WSMsgTyp - data []byte -} - -// Parses the bytes into the message type -func (m *message) parse(b []byte) { - // the first byte represents the data type (Text, JSON, Leave) - m.typ = WSMsgTyp(b[0]) - // and others represent the data itself - m.data = b[1:] -} - -func (m *message) marshall() []byte { - return append([]byte{byte(m.typ)}, m.data...) -} - -// Converts the given bytes to string -func (m *message) readText() (string, error) { - return string(m.data), nil -} - -// Converts the given bytes to JSON -func (m *message) readJSON() (any, error) { - var v any - if err := json.Unmarshal(m.data, v); err != nil { - return nil, err - } - - return v, nil -} - type Reader interface { ReadText() (string, error) ReadJSON() (interface{}, error) diff --git a/service/jam/v2/service.go b/service/jam/v2/service.go index 7cf2d592..714fb4b9 100644 --- a/service/jam/v2/service.go +++ b/service/jam/v2/service.go @@ -62,9 +62,9 @@ func (u *User) fillDefaults() { type Jam struct { id suid.UUID owner *User - Name string `json:"name, omitempty"` - Capacity uint `json:"capacity, omitempty"` - BPM uint `json:"bpm, omitempty"` + Name string `json:"name,omitempty"` + Capacity uint `json:"capacity,omitempty"` + BPM uint `json:"bpm,omitempty"` } func (j *Jam) fillDefaults() { @@ -155,7 +155,7 @@ func (s *Service) handleP2PComms(b *websocket.Broker[Jam, User]) http.HandlerFun var u User u.fillDefaults() - conn := websocket.NewConn(rwc, &u) + conn := sub.NewConn(rwc, &u) err = sub.Connect(conn) log.Fatal(err) } From a853ec3bcd6795719da60f197c3105895566e6cf Mon Sep 17 00:00:00 2001 From: pmoieni Date: Wed, 28 Dec 2022 12:55:38 +0330 Subject: [PATCH 233/246] websocket structure cleanup --- internal/websocket/broker.go | 103 ++++++++++++++++++++++--- internal/websocket/message.go | 4 +- internal/websocket/subscriber.go | 108 +++++++++------------------ internal/websocket/websocket.go | 14 ++++ internal/websocket/websocket_test.go | 9 --- service/jam/v2/service.go | 11 ++- service/jam/v2/service_test.go | 12 +-- 7 files changed, 158 insertions(+), 103 deletions(-) delete mode 100644 internal/websocket/websocket_test.go diff --git a/internal/websocket/broker.go b/internal/websocket/broker.go index 68787862..2ee817fd 100644 --- a/internal/websocket/broker.go +++ b/internal/websocket/broker.go @@ -4,8 +4,10 @@ import ( "context" "errors" "sync" + "time" "github.com/hyphengolang/prelude/types/suid" + "golang.org/x/sync/errgroup" ) // Broker contains the list of the Subscribers @@ -19,21 +21,41 @@ type Broker[SI, CI any] struct { Context context.Context } -func NewBroker[SI, CI any](cap uint, ctx context.Context) *Broker[SI, CI] { - return &Broker[SI, CI]{ - ss: make(map[suid.UUID]*Subscriber[SI, CI]), - Capacity: cap, - Context: ctx, +func (b *Broker[SI, CI]) NewSubscriber( + cap uint, + rs int64, + rt time.Duration, + wt time.Duration, + i *SI, +) *Subscriber[SI, CI] { + return &Subscriber[SI, CI]{ + sid: suid.NewUUID(), + cs: make(map[suid.UUID]*Conn[CI]), + io: make(chan *message), + Capacity: cap, + ReadBufferSize: rs, + ReadTimeout: rt, + WriteTimeout: wt, + Info: i, + Context: b.Context, } } // Adds a new Subscriber to the list -func (b *Broker[SI, CI]) Subscribe(s *Subscriber[SI, CI]) error { - b.lock.Lock() - defer b.lock.Unlock() - b.ss[s.sid] = s +func (b *Broker[SI, CI]) Subscribe(s *Subscriber[SI, CI]) { + b.subscribe(s) +} - return nil +func (b *Broker[SI, CI]) Unsubscribe(s *Subscriber[SI, CI]) { + b.unsubscribe(s) +} + +func (b *Broker[SI, CI]) Connect(s *Subscriber[SI, CI]) error { + return b.connect(s) +} + +func (b *Broker[SI, CI]) Disconnect(s *Subscriber[SI, CI]) error { + return b.disconnect(s) } func (b *Broker[SI, CI]) GetSubscriber(sid suid.UUID) (*Subscriber[SI, CI], error) { @@ -47,3 +69,64 @@ func (b *Broker[SI, CI]) GetSubscriber(sid suid.UUID) (*Subscriber[SI, CI], erro return s, nil } + +func (b *Broker[SI, CI]) subscribe(s *Subscriber[SI, CI]) { + b.lock.Lock() + defer b.lock.Unlock() + b.ss[s.sid] = s +} + +func (b *Broker[SI, CI]) unsubscribe(s *Subscriber[SI, CI]) { + b.lock.Lock() + defer b.lock.Unlock() + s.online = false + close(s.io) + delete(b.ss, s.sid) +} + +func (b *Broker[SI, CI]) connect(s *Subscriber[SI, CI]) error { + if !s.online { + s.online = true + } + + g, _ := errgroup.WithContext(s.Context) + + g.Go(func() error { + for m := range s.io { + s.lock.RLock() + cs := s.cs + s.lock.RUnlock() + + for _, c := range cs { + if err := s.write(c, m.marshall()); err != nil { + return err + } + } + } + + return nil + }) + + // wait for errors + err := g.Wait() + if err != nil { + if err := b.disconnect(s); err != nil { + return err + } + + b.unsubscribe(s) + return err + } + + return nil +} + +func (b *Broker[SI, CI]) disconnect(s *Subscriber[SI, CI]) error { + for _, c := range s.cs { + if err := s.disconnect(c); err != nil { + return err + } + } + + return nil +} diff --git a/internal/websocket/message.go b/internal/websocket/message.go index 0088b22e..afaf6478 100644 --- a/internal/websocket/message.go +++ b/internal/websocket/message.go @@ -1,6 +1,8 @@ package websocket -import "encoding/json" +import ( + "encoding/json" +) type WSMsgTyp int diff --git a/internal/websocket/subscriber.go b/internal/websocket/subscriber.go index 7487cc60..6804a8e2 100644 --- a/internal/websocket/subscriber.go +++ b/internal/websocket/subscriber.go @@ -35,28 +35,20 @@ type Subscriber[SI, CI any] struct { Context context.Context } -func (b *Broker[SI, CI]) NewSubscriber( - cap uint, - rs int64, - rt time.Duration, - wt time.Duration, - i *SI, -) *Subscriber[SI, CI] { - return &Subscriber[SI, CI]{ - sid: suid.NewUUID(), - cs: make(map[suid.UUID]*Conn[CI]), - io: make(chan *message), - Capacity: cap, - ReadBufferSize: rs, - ReadTimeout: rt, - WriteTimeout: wt, - Info: i, - Context: b.Context, +func (s *Subscriber[SI, CI]) NewConn(rwc io.ReadWriteCloser, info *CI) *Conn[CI] { + return &Conn[CI]{ + sid: suid.NewUUID(), + rwc: rwc, + Info: info, } } -func (s *Subscriber[SI, CI]) Listen() error { - return s.listen() +func (s *Subscriber[SI, CI]) Subscribe(c *Conn[CI]) { + s.subscribe(c) +} + +func (s *Subscriber[SI, CI]) Unsubscribe(c *Conn[CI]) { + s.unsubscribe(c) } func (s *Subscriber[SI, CI]) Connect(c *Conn[CI]) error { @@ -75,62 +67,42 @@ func (s *Subscriber[SI, CI]) IsFull() bool { return len(s.cs) >= int(s.Capacity) } -func (s *Subscriber[SI, CI]) NewConn(rwc io.ReadWriteCloser, info *CI) *Conn[CI] { - return &Conn[CI]{ - sid: suid.NewUUID(), - rwc: rwc, - Info: info, - } -} - func (s *Subscriber[SI, CI]) GetID() suid.UUID { return s.sid } -// Starts listening on the io channel which Connections send their messages to -func (s *Subscriber[SI, CI]) listen() error { - s.online = true - - for m := range s.io { - s.lock.RLock() - cs := s.cs - s.lock.Unlock() - - for _, c := range cs { - if err := s.write(c, m.marshall()); err != nil { - return err - } - } - } +func (s *Subscriber[SI, CI]) subscribe(c *Conn[CI]) { + s.lock.Lock() + defer s.lock.Unlock() - return nil + // add the connection to the list + s.cs[c.sid] = c } -// Connects the given Connection to the Subscriber and adds it to the list of its Connections -func (s *Subscriber[SI, CI]) connect(c *Conn[CI]) error { +func (s *Subscriber[SI, CI]) unsubscribe(c *Conn[CI]) { s.lock.Lock() defer s.lock.Unlock() - // check if the Subscriber is listening - if s.online { - // add the connection to the list - s.cs[c.sid] = c - - // create an error group to catch goroutine errors - g, _ := errgroup.WithContext(s.Context) - g.Go(func() error { - return s.read(c) - }) - - // wait for errors - err := g.Wait() - if err != nil { - if err := s.disconnect(c); err != nil { - return err - } + // remove connection from the list + delete(s.cs, c.sid) +} +// Connects the given Connection to the Subscriber and adds it to the list of its Connections +func (s *Subscriber[SI, CI]) connect(c *Conn[CI]) error { + // create an error group to catch goroutine errors + g, _ := errgroup.WithContext(s.Context) + g.Go(func() error { + return s.read(c) + }) + + // wait for errors + err := g.Wait() + if err != nil { + if err := s.disconnect(c); err != nil { return err } + + return err } return nil @@ -139,13 +111,7 @@ func (s *Subscriber[SI, CI]) connect(c *Conn[CI]) error { // Closes the given Connection and removes it from the Connections list func (s *Subscriber[SI, CI]) disconnect(c *Conn[CI]) error { // close websocket connection - if err := c.rwc.Close(); err != nil { - return err - } - - // remove connection from the list - delete(s.cs, c.sid) - return nil + return c.rwc.Close() } // Starts reading from the given Connection @@ -159,14 +125,14 @@ func (s *Subscriber[SI, CI]) read(c *Conn[CI]) error { return err } - var m *message + var m message m.parse(b) switch m.typ { case Leave: return s.disconnect(c) default: - s.io <- m + s.io <- &m } return nil diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 1c792001..5d011161 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -1,5 +1,19 @@ package websocket +import ( + "context" + + "github.com/hyphengolang/prelude/types/suid" +) + +func NewBroker[SI, CI any](cap uint, ctx context.Context) *Broker[SI, CI] { + return &Broker[SI, CI]{ + ss: make(map[suid.UUID]*Subscriber[SI, CI]), + Capacity: cap, + Context: ctx, + } +} + type Reader interface { ReadText() (string, error) ReadJSON() (interface{}, error) diff --git a/internal/websocket/websocket_test.go b/internal/websocket/websocket_test.go deleted file mode 100644 index 43b4db4c..00000000 --- a/internal/websocket/websocket_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package websocket - -// func TestWebsocket(t *testing.T) { -// is := is.New(t) -// mux := chi.NewMux() -// srv := httptest.NewServer(mux) - -// t.Run("Create a new jam session") -// } diff --git a/service/jam/v2/service.go b/service/jam/v2/service.go index 714fb4b9..24e5334c 100644 --- a/service/jam/v2/service.go +++ b/service/jam/v2/service.go @@ -98,12 +98,10 @@ func (s *Service) handleCreateJamRoom(b *websocket.Broker[Jam, User]) http.Handl sub := b.NewSubscriber(j.Capacity, 512, defaultTimeout, defaultTimeout, &j) // add the Subscriber to Broker - if err := b.Subscribe(sub); err != nil { - s.Respond(w, r, err, http.StatusInternalServerError) - return - } + b.Subscribe(sub) - if err := sub.Listen(); err != nil { + // connect the Subscriber + if err := b.Connect(sub); err != nil { s.Respond(w, r, err, http.StatusInternalServerError) return } @@ -128,7 +126,7 @@ func (s *Service) handleListRooms(b *websocket.Broker[Jam, User]) http.HandlerFu func (s *Service) handleP2PComms(b *websocket.Broker[Jam, User]) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - // decode uuid from + // decode uuid from URL sid, err := s.parseUUID(w, r) if err != nil { s.Respond(w, r, sid, http.StatusBadRequest) @@ -156,6 +154,7 @@ func (s *Service) handleP2PComms(b *websocket.Broker[Jam, User]) http.HandlerFun u.fillDefaults() conn := sub.NewConn(rwc, &u) + sub.Subscribe(conn) err = sub.Connect(conn) log.Fatal(err) } diff --git a/service/jam/v2/service_test.go b/service/jam/v2/service_test.go index 242cf4a9..6710a547 100644 --- a/service/jam/v2/service_test.go +++ b/service/jam/v2/service_test.go @@ -42,7 +42,6 @@ func TestService(t *testing.T) { is.NoErr(err) // retrieve location firstJam = resource(loc.Path) - t.Log(firstJam) }) t.Run(`Connect to Jam room with id: `+firstJam, func(t *testing.T) { @@ -51,15 +50,16 @@ func TestService(t *testing.T) { t.Cleanup(func() { c1.Close() }) - text := "Hello, World!" + data := []byte("Hello World!") + typ := []byte{1} + m := append(typ, data...) - err = wsutil.WriteClientBinary(c1, []byte(text)) + err = wsutil.WriteClientBinary(c1, m) is.NoErr(err) // write to pool - data, err := wsutil.ReadServerBinary(c1) + res, err := wsutil.ReadServerBinary(c1) is.NoErr(err) // read from pool - is.Equal(string(data), text) + is.Equal(res, m) }) - } From b101236feabed26d6b8fe6e5ebd9624f9710deaa Mon Sep 17 00:00:00 2001 From: pmoieni Date: Thu, 29 Dec 2022 20:47:54 +0330 Subject: [PATCH 234/246] websocket revamp & new error type --- internal/websocket/broker.go | 82 ++++-------- internal/websocket/conn.go | 9 ++ internal/websocket/error.go | 10 ++ internal/websocket/subscriber.go | 162 ++++++++++++++---------- internal/websocket/websocket.go | 1 + internal/websocket/websocket_test.go | 182 +++++++++++++++++++++++++++ service/jam/v2/service.go | 11 +- service/jam/v2/service_test.go | 36 +++--- 8 files changed, 348 insertions(+), 145 deletions(-) create mode 100644 internal/websocket/error.go create mode 100644 internal/websocket/websocket_test.go diff --git a/internal/websocket/broker.go b/internal/websocket/broker.go index 2ee817fd..00388bcd 100644 --- a/internal/websocket/broker.go +++ b/internal/websocket/broker.go @@ -4,10 +4,8 @@ import ( "context" "errors" "sync" - "time" "github.com/hyphengolang/prelude/types/suid" - "golang.org/x/sync/errgroup" ) // Broker contains the list of the Subscribers @@ -15,43 +13,30 @@ type Broker[SI, CI any] struct { lock sync.RWMutex // list of Subscribers ss map[suid.UUID]*Subscriber[SI, CI] + // error channel + errc chan error // Maximum Capacity Subscribers allowed Capacity uint Context context.Context } -func (b *Broker[SI, CI]) NewSubscriber( - cap uint, - rs int64, - rt time.Duration, - wt time.Duration, - i *SI, -) *Subscriber[SI, CI] { - return &Subscriber[SI, CI]{ - sid: suid.NewUUID(), - cs: make(map[suid.UUID]*Conn[CI]), - io: make(chan *message), - Capacity: cap, - ReadBufferSize: rs, - ReadTimeout: rt, - WriteTimeout: wt, - Info: i, - Context: b.Context, - } -} - // Adds a new Subscriber to the list func (b *Broker[SI, CI]) Subscribe(s *Subscriber[SI, CI]) { - b.subscribe(s) + b.connect(s) + b.add(s) } -func (b *Broker[SI, CI]) Unsubscribe(s *Subscriber[SI, CI]) { - b.unsubscribe(s) +func (b *Broker[SI, CI]) Unsubscribe(s *Subscriber[SI, CI]) error { + if err := b.disconnect(s); err != nil { + return err + } + b.remove(s) + return nil } -func (b *Broker[SI, CI]) Connect(s *Subscriber[SI, CI]) error { - return b.connect(s) +func (b *Broker[SI, CI]) Connect(s *Subscriber[SI, CI]) { + b.connect(s) } func (b *Broker[SI, CI]) Disconnect(s *Subscriber[SI, CI]) error { @@ -70,58 +55,43 @@ func (b *Broker[SI, CI]) GetSubscriber(sid suid.UUID) (*Subscriber[SI, CI], erro return s, nil } -func (b *Broker[SI, CI]) subscribe(s *Subscriber[SI, CI]) { +func (b *Broker[SI, CI]) add(s *Subscriber[SI, CI]) { b.lock.Lock() defer b.lock.Unlock() b.ss[s.sid] = s } -func (b *Broker[SI, CI]) unsubscribe(s *Subscriber[SI, CI]) { +func (b *Broker[SI, CI]) remove(s *Subscriber[SI, CI]) { b.lock.Lock() defer b.lock.Unlock() - s.online = false - close(s.io) + close(s.ic) + close(s.oc) + close(s.errc) delete(b.ss, s.sid) } -func (b *Broker[SI, CI]) connect(s *Subscriber[SI, CI]) error { +func (b *Broker[SI, CI]) connect(s *Subscriber[SI, CI]) { if !s.online { s.online = true } - g, _ := errgroup.WithContext(s.Context) - - g.Go(func() error { - for m := range s.io { - s.lock.RLock() + go func() { + for m := range s.ic { + // s.lock.RLock() cs := s.cs - s.lock.RUnlock() + // s.lock.RUnlock() for _, c := range cs { - if err := s.write(c, m.marshall()); err != nil { - return err + if err := c.write(m.marshall()); err != nil { + s.errc <- &wserr[CI]{c, err} } } } - - return nil - }) - - // wait for errors - err := g.Wait() - if err != nil { - if err := b.disconnect(s); err != nil { - return err - } - - b.unsubscribe(s) - return err - } - - return nil + }() } func (b *Broker[SI, CI]) disconnect(s *Subscriber[SI, CI]) error { + s.online = false for _, c := range s.cs { if err := s.disconnect(c); err != nil { return err diff --git a/internal/websocket/conn.go b/internal/websocket/conn.go index 5bf7878b..536d0e64 100644 --- a/internal/websocket/conn.go +++ b/internal/websocket/conn.go @@ -4,6 +4,7 @@ import ( "io" "sync" + "github.com/gobwas/ws/wsutil" "github.com/hyphengolang/prelude/types/suid" ) @@ -15,3 +16,11 @@ type Conn[CI any] struct { Info *CI } + +// Writes raw bytes to the Connection +func (c *Conn[CI]) write(b []byte) error { + c.lock.RLock() + defer c.lock.RUnlock() + + return wsutil.WriteServerBinary(c.rwc, b) +} diff --git a/internal/websocket/error.go b/internal/websocket/error.go new file mode 100644 index 00000000..2115b4ff --- /dev/null +++ b/internal/websocket/error.go @@ -0,0 +1,10 @@ +package websocket + +type wserr[CI any] struct { + conn *Conn[CI] + msg error +} + +func (e *wserr[CI]) Error() string { + return e.msg.Error() +} diff --git a/internal/websocket/subscriber.go b/internal/websocket/subscriber.go index 6804a8e2..90ebc5b8 100644 --- a/internal/websocket/subscriber.go +++ b/internal/websocket/subscriber.go @@ -3,12 +3,12 @@ package websocket import ( "context" "io" + "log" "sync" "time" "github.com/gobwas/ws/wsutil" "github.com/hyphengolang/prelude/types/suid" - "golang.org/x/sync/errgroup" ) // Subscriber contains the list of the connections @@ -21,7 +21,10 @@ type Subscriber[SI, CI any] struct { // Subscriber status online bool // Input/Output channel for new messages - io chan *message + ic chan *message + oc chan *message + // error channel + errc chan *wserr[CI] // Maximum Capacity clients allowed Capacity uint // Maximum message size allowed from peer. @@ -35,6 +38,35 @@ type Subscriber[SI, CI any] struct { Context context.Context } +func NewSubscriber[SI, CI any]( + ctx context.Context, + cap uint, + rs int64, + rt time.Duration, + wt time.Duration, + i *SI, +) (*Subscriber[SI, CI], error) { + s := &Subscriber[SI, CI]{ + sid: suid.NewUUID(), + cs: make(map[suid.UUID]*Conn[CI]), + // I did make + ic: make(chan *message), + oc: make(chan *message), + errc: make(chan *wserr[CI]), + Capacity: cap, + ReadBufferSize: rs, + ReadTimeout: rt, + WriteTimeout: wt, + Info: i, + Context: ctx, + } + + s.catch() + s.listen() + + return s, nil +} + func (s *Subscriber[SI, CI]) NewConn(rwc io.ReadWriteCloser, info *CI) *Conn[CI] { return &Conn[CI]{ sid: suid.NewUUID(), @@ -44,20 +76,25 @@ func (s *Subscriber[SI, CI]) NewConn(rwc io.ReadWriteCloser, info *CI) *Conn[CI] } func (s *Subscriber[SI, CI]) Subscribe(c *Conn[CI]) { - s.subscribe(c) + s.connect(c) + s.add(c) } -func (s *Subscriber[SI, CI]) Unsubscribe(c *Conn[CI]) { - s.unsubscribe(c) +func (s *Subscriber[SI, CI]) Unsubscribe(c *Conn[CI]) error { + if err := s.disconnect(c); err != nil { + return err + } + s.remove(c) + return nil } -func (s *Subscriber[SI, CI]) Connect(c *Conn[CI]) error { - return s.connect(c) -} +// func (s *Subscriber[SI, CI]) Connect(c *Conn[CI]) error { +// return s.connect(c) +// } -func (s *Subscriber[SI, CI]) Disconnect(c *Conn[CI]) error { - return s.disconnect(c) -} +// func (s *Subscriber[SI, CI]) Disconnect(c *Conn[CI]) error { +// return s.disconnect(c) +// } func (s *Subscriber[SI, CI]) IsFull() bool { if s.Capacity == 0 { @@ -71,7 +108,31 @@ func (s *Subscriber[SI, CI]) GetID() suid.UUID { return s.sid } -func (s *Subscriber[SI, CI]) subscribe(c *Conn[CI]) { +// listen to the input channel and broadcast messages to clients. +func (s *Subscriber[SI, CI]) listen() { + go func() { + for p := range s.ic { + for _, c := range s.cs { + if err := c.write(p.marshall()); err != nil { + s.errc <- &wserr[CI]{c, err} + } + } + } + }() +} + +func (s *Subscriber[SI, CI]) catch() { + go func() { + for e := range s.errc { + + if err := s.disconnect(e.conn); err != nil { + log.Println(err) + } + } + }() +} + +func (s *Subscriber[SI, CI]) add(c *Conn[CI]) { s.lock.Lock() defer s.lock.Unlock() @@ -79,7 +140,7 @@ func (s *Subscriber[SI, CI]) subscribe(c *Conn[CI]) { s.cs[c.sid] = c } -func (s *Subscriber[SI, CI]) unsubscribe(c *Conn[CI]) { +func (s *Subscriber[SI, CI]) remove(c *Conn[CI]) { s.lock.Lock() defer s.lock.Unlock() @@ -87,25 +148,32 @@ func (s *Subscriber[SI, CI]) unsubscribe(c *Conn[CI]) { delete(s.cs, c.sid) } -// Connects the given Connection to the Subscriber and adds it to the list of its Connections -func (s *Subscriber[SI, CI]) connect(c *Conn[CI]) error { - // create an error group to catch goroutine errors - g, _ := errgroup.WithContext(s.Context) - g.Go(func() error { - return s.read(c) - }) - - // wait for errors - err := g.Wait() - if err != nil { - if err := s.disconnect(c); err != nil { - return err - } - - return err - } +// Connects the given Connection to the Subscriber and starts reading from it +func (s *Subscriber[SI, CI]) connect(c *Conn[CI]) { + c.lock.RLock() + defer c.lock.RUnlock() - return nil + go func() { + for { + // read binary from connection + b, err := wsutil.ReadClientBinary(c.rwc) + if err != nil { + s.errc <- &wserr[CI]{c, err} + } + + var m message + m.parse(b) + + switch m.typ { + case Leave: + if err := s.disconnect(c); err != nil { + s.errc <- &wserr[CI]{c, err} + } + default: + s.ic <- &m + } + } + }() } // Closes the given Connection and removes it from the Connections list @@ -113,35 +181,3 @@ func (s *Subscriber[SI, CI]) disconnect(c *Conn[CI]) error { // close websocket connection return c.rwc.Close() } - -// Starts reading from the given Connection -func (s *Subscriber[SI, CI]) read(c *Conn[CI]) error { - c.lock.RLock() - defer c.lock.RUnlock() - - // read binary from connection - b, err := wsutil.ReadClientBinary(c.rwc) - if err != nil { - return err - } - - var m message - m.parse(b) - - switch m.typ { - case Leave: - return s.disconnect(c) - default: - s.io <- &m - } - - return nil -} - -// Writes raw bytes to the Connection -func (s *Subscriber[SI, CI]) write(c *Conn[CI], b []byte) error { - c.lock.RLock() - defer c.lock.RUnlock() - - return wsutil.WriteServerBinary(c.rwc, b) -} diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 5d011161..b4c6ac1c 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -9,6 +9,7 @@ import ( func NewBroker[SI, CI any](cap uint, ctx context.Context) *Broker[SI, CI] { return &Broker[SI, CI]{ ss: make(map[suid.UUID]*Subscriber[SI, CI]), + errc: make(chan error), Capacity: cap, Context: ctx, } diff --git a/internal/websocket/websocket_test.go b/internal/websocket/websocket_test.go new file mode 100644 index 00000000..4cfbee93 --- /dev/null +++ b/internal/websocket/websocket_test.go @@ -0,0 +1,182 @@ +package websocket_test + +// ok +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/go-chi/chi" + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" + "github.com/hyphengolang/prelude/testing/is" + "github.com/hyphengolang/prelude/types/suid" + "github.com/rog-golang-buddies/rmx/internal/websocket" +) + +// so I am defining a simple echo server here for testing +func testServerPartA() http.Handler { + ctx := context.Background() + + s, _ := websocket.NewSubscriber[any, any]( + ctx, + 2, + 512, + 2*time.Second, + 2*time.Second, + nil, + ) + + mux := http.NewServeMux() + + mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + return + } + + wsc := s.NewConn(conn, nil) + s.Subscribe(wsc) + }) + + return mux +} + +func TestSubscriber(t *testing.T) { + // t.Skip() + + is := is.New(t) + ctx := context.Background() + + srv := httptest.NewServer(testServerPartA()) + + t.Cleanup(func() { + srv.Close() + }) + + // NOTE - can you try this test for me please? passed, really? + t.Run("create a new client and connect to echo server", func(t *testing.T) { + wsPath := stripPrefix(srv.URL + "/ws") // this correct right? yup + + cli1, _, _, err := ws.DefaultDialer.Dial(ctx, wsPath) + is.NoErr(err) // connect cli1 to server + defer cli1.Close() // ok + + cli2, _, _, err := ws.DefaultDialer.Dial(ctx, wsPath) + is.NoErr(err) // connect cli2 to server + defer cli2.Close() // ok + + _, _, _, err = ws.DefaultDialer.Dial(ctx, wsPath) + is.NoErr(err) // cannot connect to the server + + data := []byte("Hello World!") + typ := []byte{1} + m := append(typ, data...) + + err = wsutil.WriteClientBinary(cli1, m) + is.NoErr(err) // send message to server + + // now I want to read the message from the server + msg, err := wsutil.ReadServerBinary(cli2) + is.NoErr(err) // read message from server + is.Equal(m, msg) // check if message is correct + }) +} + +func testServerPartB() http.Handler { + ctx := context.Background() + + type Info struct { + Username string + } + + b := websocket.NewBroker[Info, any](3, ctx) + + mux := http.NewServeMux() + + mux.HandleFunc("/create", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + s, _ := websocket.NewSubscriber[Info, any](ctx, 2, 512, 2*time.Second, 2*time.Second, &Info{ + Username: "John Doe", + }) + + w.Header().Set("Location", "/"+s.GetID().ShortUUID().String()) + }) + + // so I need to get the subscriber from parsing here right? yes + mux.HandleFunc("/ws/{suid}", func(w http.ResponseWriter, r *http.Request) { + sid, err := parseSUID(w, r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + // can you see me? + + s, err := b.GetSubscriber(sid) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + + conn, _, _, err := ws.UpgradeHTTP(r, w) + if err != nil { + return + } + + wsc := s.NewConn(conn, nil) + s.Subscribe(wsc) + }) + + return mux +} + +func TestBroker(t *testing.T) { + is := is.New(t) + + srv := httptest.NewServer(testServerPartB()) + + t.Cleanup(func() { + srv.Close() + }) + + t.Run("create a new subscriber", func(t *testing.T) { + srv.Client().Post(srv.URL+"/create", "application/json", nil) + + is.NoErr(nil) + }) + + t.Run("connect to subscriber", func(t *testing.T) { + t.Skip() + + is.NoErr(nil) + }) + + t.Run("delete a subscriber", func(t *testing.T) { + t.Skip() + is.NoErr(nil) + }) +} + +var resource = func(s string) string { + return s[strings.LastIndex(s, "/")+1:] +} + +var stripPrefix = func(s string) string { + return "ws" + strings.TrimPrefix(s, "http") +} + +func parseSUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { + return suid.ParseString(chi.URLParam(r, "uuid")) +} + +/* + + */ diff --git a/service/jam/v2/service.go b/service/jam/v2/service.go index 24e5334c..a4da9b30 100644 --- a/service/jam/v2/service.go +++ b/service/jam/v2/service.go @@ -97,11 +97,8 @@ func (s *Service) handleCreateJamRoom(b *websocket.Broker[Jam, User]) http.Handl // create a new Subscriber sub := b.NewSubscriber(j.Capacity, 512, defaultTimeout, defaultTimeout, &j) - // add the Subscriber to Broker - b.Subscribe(sub) - // connect the Subscriber - if err := b.Connect(sub); err != nil { + if err := b.Subscribe(sub); err != nil { s.Respond(w, r, err, http.StatusInternalServerError) return } @@ -154,9 +151,9 @@ func (s *Service) handleP2PComms(b *websocket.Broker[Jam, User]) http.HandlerFun u.fillDefaults() conn := sub.NewConn(rwc, &u) - sub.Subscribe(conn) - err = sub.Connect(conn) - log.Fatal(err) + if err := sub.Subscribe(conn); err != nil { + log.Fatal(err) + } } } diff --git a/service/jam/v2/service_test.go b/service/jam/v2/service_test.go index 6710a547..1e4ab833 100644 --- a/service/jam/v2/service_test.go +++ b/service/jam/v2/service_test.go @@ -8,8 +8,6 @@ import ( "testing" "github.com/go-chi/chi/v5" - "github.com/gobwas/ws" - "github.com/gobwas/ws/wsutil" "github.com/hyphengolang/prelude/testing/is" ) @@ -27,7 +25,7 @@ func TestService(t *testing.T) { h := NewService(ctx, mux) srv := httptest.NewServer(h) - var firstJam string + // var firstJam string t.Run("Create a new Jam room", func(t *testing.T) { payload := `{ "name": "John Doe", @@ -38,28 +36,28 @@ func TestService(t *testing.T) { res, _ := srv.Client().Post(srv.URL+"/api/v1/jam", "application/json", strings.NewReader(payload)) is.Equal(res.StatusCode, http.StatusCreated) // created a new resource - loc, err := res.Location() - is.NoErr(err) // retrieve location + // loc, err := res.Location() + // // is.NoErr(err) // retrieve location - firstJam = resource(loc.Path) + // // firstJam = resource(loc.Path) }) - t.Run(`Connect to Jam room with id: `+firstJam, func(t *testing.T) { - c1, _, _, err := ws.DefaultDialer.Dial(ctx, stripPrefix(srv.URL+"/ws/jam/"+firstJam)) - is.NoErr(err) // found first jam Session + // t.Run(`Connect to Jam room with id: `+firstJam, func(t *testing.T) { + // c1, _, _, err := ws.DefaultDialer.Dial(ctx, stripPrefix(srv.URL+"/ws/jam/"+firstJam)) + // is.NoErr(err) // found first jam Session - t.Cleanup(func() { c1.Close() }) + // t.Cleanup(func() { c1.Close() }) - data := []byte("Hello World!") - typ := []byte{1} - m := append(typ, data...) + // data := []byte("Hello World!") + // typ := []byte{1} + // m := append(typ, data...) - err = wsutil.WriteClientBinary(c1, m) - is.NoErr(err) // write to pool + // err = wsutil.WriteClientBinary(c1, m) + // is.NoErr(err) // write to pool - res, err := wsutil.ReadServerBinary(c1) - is.NoErr(err) // read from pool + // res, err := wsutil.ReadServerBinary(c1) + // is.NoErr(err) // read from pool - is.Equal(res, m) - }) + // is.Equal(res, m) + // }) } From 8aa6e68a67b7cfd15164f3c8dbb7763a6c26b2a8 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Thu, 29 Dec 2022 20:55:33 +0330 Subject: [PATCH 235/246] fix jam service --- service/jam/v2/service.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/service/jam/v2/service.go b/service/jam/v2/service.go index a4da9b30..2deec6a6 100644 --- a/service/jam/v2/service.go +++ b/service/jam/v2/service.go @@ -3,7 +3,6 @@ package v2 import ( "context" "errors" - "log" "net/http" "strings" "time" @@ -95,14 +94,15 @@ func (s *Service) handleCreateJamRoom(b *websocket.Broker[Jam, User]) http.Handl j.fillDefaults() // create a new Subscriber - sub := b.NewSubscriber(j.Capacity, 512, defaultTimeout, defaultTimeout, &j) - - // connect the Subscriber - if err := b.Subscribe(sub); err != nil { + sub, err := websocket.NewSubscriber[Jam, User](b.Context, j.Capacity, 512, defaultTimeout, defaultTimeout, &j) + if err != nil { s.Respond(w, r, err, http.StatusInternalServerError) return } + // connect the Subscriber + b.Subscribe(sub) + s.Created(w, r, sub.GetID().ShortUUID().String()) } } @@ -151,9 +151,7 @@ func (s *Service) handleP2PComms(b *websocket.Broker[Jam, User]) http.HandlerFun u.fillDefaults() conn := sub.NewConn(rwc, &u) - if err := sub.Subscribe(conn); err != nil { - log.Fatal(err) - } + sub.Subscribe(conn) } } From 220e67ecdf07e66c535f9b42fe3d71f83f3aadb1 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Thu, 29 Dec 2022 20:57:24 +0330 Subject: [PATCH 236/246] test public API for jam service --- service/jam/v2/service_test.go | 36 ++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/service/jam/v2/service_test.go b/service/jam/v2/service_test.go index 1e4ab833..6710a547 100644 --- a/service/jam/v2/service_test.go +++ b/service/jam/v2/service_test.go @@ -8,6 +8,8 @@ import ( "testing" "github.com/go-chi/chi/v5" + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" "github.com/hyphengolang/prelude/testing/is" ) @@ -25,7 +27,7 @@ func TestService(t *testing.T) { h := NewService(ctx, mux) srv := httptest.NewServer(h) - // var firstJam string + var firstJam string t.Run("Create a new Jam room", func(t *testing.T) { payload := `{ "name": "John Doe", @@ -36,28 +38,28 @@ func TestService(t *testing.T) { res, _ := srv.Client().Post(srv.URL+"/api/v1/jam", "application/json", strings.NewReader(payload)) is.Equal(res.StatusCode, http.StatusCreated) // created a new resource - // loc, err := res.Location() - // // is.NoErr(err) // retrieve location + loc, err := res.Location() + is.NoErr(err) // retrieve location - // // firstJam = resource(loc.Path) + firstJam = resource(loc.Path) }) - // t.Run(`Connect to Jam room with id: `+firstJam, func(t *testing.T) { - // c1, _, _, err := ws.DefaultDialer.Dial(ctx, stripPrefix(srv.URL+"/ws/jam/"+firstJam)) - // is.NoErr(err) // found first jam Session + t.Run(`Connect to Jam room with id: `+firstJam, func(t *testing.T) { + c1, _, _, err := ws.DefaultDialer.Dial(ctx, stripPrefix(srv.URL+"/ws/jam/"+firstJam)) + is.NoErr(err) // found first jam Session - // t.Cleanup(func() { c1.Close() }) + t.Cleanup(func() { c1.Close() }) - // data := []byte("Hello World!") - // typ := []byte{1} - // m := append(typ, data...) + data := []byte("Hello World!") + typ := []byte{1} + m := append(typ, data...) - // err = wsutil.WriteClientBinary(c1, m) - // is.NoErr(err) // write to pool + err = wsutil.WriteClientBinary(c1, m) + is.NoErr(err) // write to pool - // res, err := wsutil.ReadServerBinary(c1) - // is.NoErr(err) // read from pool + res, err := wsutil.ReadServerBinary(c1) + is.NoErr(err) // read from pool - // is.Equal(res, m) - // }) + is.Equal(res, m) + }) } From 269a64518dd02f4ce27a47b474b91f758ca31646 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Thu, 29 Dec 2022 13:06:54 -0500 Subject: [PATCH 237/246] return on errors --- internal/websocket/subscriber.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/websocket/subscriber.go b/internal/websocket/subscriber.go index 90ebc5b8..97655387 100644 --- a/internal/websocket/subscriber.go +++ b/internal/websocket/subscriber.go @@ -115,6 +115,7 @@ func (s *Subscriber[SI, CI]) listen() { for _, c := range s.cs { if err := c.write(p.marshall()); err != nil { s.errc <- &wserr[CI]{c, err} + return } } } @@ -154,11 +155,14 @@ func (s *Subscriber[SI, CI]) connect(c *Conn[CI]) { defer c.lock.RUnlock() go func() { + defer c.rwc.Close() + for { // read binary from connection b, err := wsutil.ReadClientBinary(c.rwc) if err != nil { s.errc <- &wserr[CI]{c, err} + return } var m message @@ -168,6 +172,7 @@ func (s *Subscriber[SI, CI]) connect(c *Conn[CI]) { case Leave: if err := s.disconnect(c); err != nil { s.errc <- &wserr[CI]{c, err} + return } default: s.ic <- &m From 185411ce79bd8bc532ded63df25d223aa001d8d2 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Sat, 31 Dec 2022 00:31:29 +0330 Subject: [PATCH 238/246] fix error handling --- internal/websocket/broker.go | 9 +++++++++ internal/websocket/subscriber.go | 5 ++--- internal/websocket/websocket.go | 15 --------------- internal/websocket/websocket_test.go | 4 ++-- service/jam/v2/service.go | 13 ++++++++----- 5 files changed, 21 insertions(+), 25 deletions(-) diff --git a/internal/websocket/broker.go b/internal/websocket/broker.go index 00388bcd..728f11a8 100644 --- a/internal/websocket/broker.go +++ b/internal/websocket/broker.go @@ -21,6 +21,15 @@ type Broker[SI, CI any] struct { Context context.Context } +func NewBroker[SI, CI any](cap uint, ctx context.Context) *Broker[SI, CI] { + return &Broker[SI, CI]{ + ss: make(map[suid.UUID]*Subscriber[SI, CI]), + errc: make(chan error), + Capacity: cap, + Context: ctx, + } +} + // Adds a new Subscriber to the list func (b *Broker[SI, CI]) Subscribe(s *Subscriber[SI, CI]) { b.connect(s) diff --git a/internal/websocket/subscriber.go b/internal/websocket/subscriber.go index 97655387..9c6f48b0 100644 --- a/internal/websocket/subscriber.go +++ b/internal/websocket/subscriber.go @@ -45,7 +45,7 @@ func NewSubscriber[SI, CI any]( rt time.Duration, wt time.Duration, i *SI, -) (*Subscriber[SI, CI], error) { +) *Subscriber[SI, CI] { s := &Subscriber[SI, CI]{ sid: suid.NewUUID(), cs: make(map[suid.UUID]*Conn[CI]), @@ -64,7 +64,7 @@ func NewSubscriber[SI, CI any]( s.catch() s.listen() - return s, nil + return s } func (s *Subscriber[SI, CI]) NewConn(rwc io.ReadWriteCloser, info *CI) *Conn[CI] { @@ -125,7 +125,6 @@ func (s *Subscriber[SI, CI]) listen() { func (s *Subscriber[SI, CI]) catch() { go func() { for e := range s.errc { - if err := s.disconnect(e.conn); err != nil { log.Println(err) } diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index b4c6ac1c..1c792001 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -1,20 +1,5 @@ package websocket -import ( - "context" - - "github.com/hyphengolang/prelude/types/suid" -) - -func NewBroker[SI, CI any](cap uint, ctx context.Context) *Broker[SI, CI] { - return &Broker[SI, CI]{ - ss: make(map[suid.UUID]*Subscriber[SI, CI]), - errc: make(chan error), - Capacity: cap, - Context: ctx, - } -} - type Reader interface { ReadText() (string, error) ReadJSON() (interface{}, error) diff --git a/internal/websocket/websocket_test.go b/internal/websocket/websocket_test.go index 4cfbee93..f2d6bf3f 100644 --- a/internal/websocket/websocket_test.go +++ b/internal/websocket/websocket_test.go @@ -21,7 +21,7 @@ import ( func testServerPartA() http.Handler { ctx := context.Background() - s, _ := websocket.NewSubscriber[any, any]( + s := websocket.NewSubscriber[any, any]( ctx, 2, 512, @@ -103,7 +103,7 @@ func testServerPartB() http.Handler { return } - s, _ := websocket.NewSubscriber[Info, any](ctx, 2, 512, 2*time.Second, 2*time.Second, &Info{ + s := websocket.NewSubscriber[Info, any](ctx, 2, 512, 2*time.Second, 2*time.Second, &Info{ Username: "John Doe", }) diff --git a/service/jam/v2/service.go b/service/jam/v2/service.go index 2deec6a6..3087927c 100644 --- a/service/jam/v2/service.go +++ b/service/jam/v2/service.go @@ -94,11 +94,14 @@ func (s *Service) handleCreateJamRoom(b *websocket.Broker[Jam, User]) http.Handl j.fillDefaults() // create a new Subscriber - sub, err := websocket.NewSubscriber[Jam, User](b.Context, j.Capacity, 512, defaultTimeout, defaultTimeout, &j) - if err != nil { - s.Respond(w, r, err, http.StatusInternalServerError) - return - } + sub := websocket.NewSubscriber[Jam, User]( + b.Context, + j.Capacity, + 512, + defaultTimeout, + defaultTimeout, + &j, + ) // connect the Subscriber b.Subscribe(sub) From 9aa2f99da073e9ae2f6f781e6968767b93975ad0 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Sat, 31 Dec 2022 14:10:47 -0500 Subject: [PATCH 239/246] Split the RMX CLI and server application into two commands --- .gitignore | 4 ++-- .vscode/launch.json | 12 ++++++++++-- cmd/{ => cli}/main.go | 0 cmd/server/main.go | 26 ++++++++++++++++++++++++++ config/config.go | 37 +++++++++++++++++++++++++++++++++++++ internal/commands/run.go | 5 +++++ 6 files changed, 80 insertions(+), 4 deletions(-) rename cmd/{ => cli}/main.go (100%) create mode 100644 cmd/server/main.go diff --git a/.gitignore b/.gitignore index a89410f5..2efecd91 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ bin/ # Other .DS_Store -*.env +*.env* certs rmx.config.json -rmx-dev.config.json \ No newline at end of file +rmx-dev.config.json diff --git a/.vscode/launch.json b/.vscode/launch.json index 9c19b925..72e63361 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,9 +9,17 @@ "type": "go", "request": "launch", "mode": "auto", - "program": "${workspaceFolder}/cmd", + "program": "${workspaceFolder}/cmd/server", + "cwd": "${workspaceFolder}", + "envFile": "${workspaceFolder}/.env.development" + }, + { + "name": "Start CLI Server (dev)", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/cli", "cwd": "${workspaceFolder}", - "envFile": "${workspaceFolder}/cmd/.env", "args": ["start", "dev"] }, { diff --git a/cmd/main.go b/cmd/cli/main.go similarity index 100% rename from cmd/main.go rename to cmd/cli/main.go diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 00000000..ff7e94cf --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "log" + "os" + + "github.com/rog-golang-buddies/rmx/config" + "github.com/rog-golang-buddies/rmx/internal/commands" +) + +func main() { + rmxEnv := os.Getenv("RMX_ENV") + isDev := false + if rmxEnv == "development" { + isDev = true + } + cfg, err := config.LoadConfigFromEnv(isDev) + if err != nil { + log.Fatalf("Could load config: %v", err) + } + + err = commands.StartServer(cfg) + if err != nil { + log.Fatalf("Could not start server: %v", err) + } +} diff --git a/config/config.go b/config/config.go index f02465cc..5452e74f 100644 --- a/config/config.go +++ b/config/config.go @@ -3,7 +3,9 @@ package config import ( "encoding/json" "errors" + "fmt" "log" + "net/url" "os" ) @@ -87,3 +89,38 @@ func ScanConfigFile(dev bool) (*Config, error) { return c, nil } + +// LoadConfigFromEnv creates a Config from environment variables. +func LoadConfigFromEnv(dev bool) (*Config, error) { + serverPort := os.Getenv("PORT") + + pgURI := os.Getenv("POSTGRES_URI") + + pgParsed, err := url.Parse(pgURI) + if err != nil && pgURI != "" { + return nil, fmt.Errorf("invalid POSTGRES_URL env var: %q: %w", pgURI, err) + } + + pgUser := pgParsed.User.Username() + pgPassword, _ := pgParsed.User.Password() + + pgHost := pgParsed.Host + pgPort := pgParsed.Port() + pgName := pgParsed.Path + + redisHost := os.Getenv("REDIS_HOST") + redisPort := os.Getenv("REDIS_PORT") + redisPassword := os.Getenv("REDIS_PASSWORD") + + return &Config{ + ServerPort: serverPort, + DBHost: pgHost, + DBPort: pgPort, + DBName: pgName, + DBUser: pgUser, + DBPassword: pgPassword, + RedisHost: redisHost, + RedisPort: redisPort, + RedisPassword: redisPassword, + }, nil +} diff --git a/internal/commands/run.go b/internal/commands/run.go index 5529b7cb..3109bfc3 100644 --- a/internal/commands/run.go +++ b/internal/commands/run.go @@ -296,3 +296,8 @@ func serve(cfg *config.Config) error { return g.Wait() } + +// StartServer starts the RMX application. +func StartServer(cfg *config.Config) error { + return serve(cfg) +} From f7d40ec1ddae465509e5c7d3815d6361649b9738 Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Sat, 31 Dec 2022 14:11:17 -0500 Subject: [PATCH 240/246] Set the token client to DefaultTokenClient --- store/store.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/store/store.go b/store/store.go index 8d5d2727..aab8751a 100644 --- a/store/store.go +++ b/store/store.go @@ -5,6 +5,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/rog-golang-buddies/rmx/internal" + "github.com/rog-golang-buddies/rmx/store/auth" "github.com/rog-golang-buddies/rmx/store/user" ) @@ -35,6 +36,7 @@ func New(ctx context.Context, connString string) (*Store, error) { s := &Store{ ur: user.NewRepo(ctx, pool), + tc: auth.DefaultTokenClient, } return s, nil From 0f6f3cb8bc3fa965512a41fd098c33f206c05d0b Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Sat, 31 Dec 2022 14:11:57 -0500 Subject: [PATCH 241/246] Move middleware registration before route registration --- service/service.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/service/service.go b/service/service.go index 6b22092f..26d48f13 100644 --- a/service/service.go +++ b/service/service.go @@ -30,10 +30,11 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.m.ServeH func New(ctx context.Context, st *store.Store) http.Handler { s := &Service{chi.NewMux(), log.Print, log.Printf, log.Fatal, log.Fatalf} + s.routes() + // TODO - use mux.Mount instead. But this works auth.NewService(ctx, s.m, st.UserRepo(), st.TokenClient()) jam.NewService(ctx, s.m) - s.routes() return s } From 3fd6b485cc2486bf42fff1b9ac28829d6fa460a8 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Sun, 1 Jan 2023 01:14:23 +0330 Subject: [PATCH 242/246] new endpoints --- internal/websocket/broker.go | 18 ++++++++++++++---- internal/websocket/error.go | 4 ++-- internal/websocket/subscriber.go | 10 +++++----- service/jam/v2/service.go | 25 +++++++++++++++++++++---- service/service.go | 2 +- 5 files changed, 43 insertions(+), 16 deletions(-) diff --git a/internal/websocket/broker.go b/internal/websocket/broker.go index 728f11a8..a35ad2d2 100644 --- a/internal/websocket/broker.go +++ b/internal/websocket/broker.go @@ -13,8 +13,6 @@ type Broker[SI, CI any] struct { lock sync.RWMutex // list of Subscribers ss map[suid.UUID]*Subscriber[SI, CI] - // error channel - errc chan error // Maximum Capacity Subscribers allowed Capacity uint @@ -24,7 +22,6 @@ type Broker[SI, CI any] struct { func NewBroker[SI, CI any](cap uint, ctx context.Context) *Broker[SI, CI] { return &Broker[SI, CI]{ ss: make(map[suid.UUID]*Subscriber[SI, CI]), - errc: make(chan error), Capacity: cap, Context: ctx, } @@ -64,6 +61,18 @@ func (b *Broker[SI, CI]) GetSubscriber(sid suid.UUID) (*Subscriber[SI, CI], erro return s, nil } +func (b *Broker[SI, CI]) ListSubscribers() []*Subscriber[SI, CI] { + b.lock.RLock() + defer b.lock.RUnlock() + + subs := make([]*Subscriber[SI, CI], 0, len(b.ss)) + for _, sub := range b.ss { + subs = append(subs, sub) + } + + return subs +} + func (b *Broker[SI, CI]) add(s *Subscriber[SI, CI]) { b.lock.Lock() defer b.lock.Unlock() @@ -92,7 +101,8 @@ func (b *Broker[SI, CI]) connect(s *Subscriber[SI, CI]) { for _, c := range cs { if err := c.write(m.marshall()); err != nil { - s.errc <- &wserr[CI]{c, err} + s.errc <- &wsErr[CI]{c, err} + return } } } diff --git a/internal/websocket/error.go b/internal/websocket/error.go index 2115b4ff..99b6e7a6 100644 --- a/internal/websocket/error.go +++ b/internal/websocket/error.go @@ -1,10 +1,10 @@ package websocket -type wserr[CI any] struct { +type wsErr[CI any] struct { conn *Conn[CI] msg error } -func (e *wserr[CI]) Error() string { +func (e *wsErr[CI]) Error() string { return e.msg.Error() } diff --git a/internal/websocket/subscriber.go b/internal/websocket/subscriber.go index 9c6f48b0..9ee958f2 100644 --- a/internal/websocket/subscriber.go +++ b/internal/websocket/subscriber.go @@ -24,7 +24,7 @@ type Subscriber[SI, CI any] struct { ic chan *message oc chan *message // error channel - errc chan *wserr[CI] + errc chan *wsErr[CI] // Maximum Capacity clients allowed Capacity uint // Maximum message size allowed from peer. @@ -52,7 +52,7 @@ func NewSubscriber[SI, CI any]( // I did make ic: make(chan *message), oc: make(chan *message), - errc: make(chan *wserr[CI]), + errc: make(chan *wsErr[CI]), Capacity: cap, ReadBufferSize: rs, ReadTimeout: rt, @@ -114,7 +114,7 @@ func (s *Subscriber[SI, CI]) listen() { for p := range s.ic { for _, c := range s.cs { if err := c.write(p.marshall()); err != nil { - s.errc <- &wserr[CI]{c, err} + s.errc <- &wsErr[CI]{c, err} return } } @@ -160,7 +160,7 @@ func (s *Subscriber[SI, CI]) connect(c *Conn[CI]) { // read binary from connection b, err := wsutil.ReadClientBinary(c.rwc) if err != nil { - s.errc <- &wserr[CI]{c, err} + s.errc <- &wsErr[CI]{c, err} return } @@ -170,7 +170,7 @@ func (s *Subscriber[SI, CI]) connect(c *Conn[CI]) { switch m.typ { case Leave: if err := s.disconnect(c); err != nil { - s.errc <- &wserr[CI]{c, err} + s.errc <- &wsErr[CI]{c, err} return } default: diff --git a/service/jam/v2/service.go b/service/jam/v2/service.go index 3087927c..2d0e478f 100644 --- a/service/jam/v2/service.go +++ b/service/jam/v2/service.go @@ -10,6 +10,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/gobwas/ws" "github.com/hyphengolang/prelude/types/suid" + "github.com/rog-golang-buddies/rmx/internal/fp" "github.com/rog-golang-buddies/rmx/internal/websocket" "github.com/rog-golang-buddies/rmx/pkg/service" ) @@ -111,23 +112,39 @@ func (s *Service) handleCreateJamRoom(b *websocket.Broker[Jam, User]) http.Handl } func (s *Service) handleGetRoomData(b *websocket.Broker[Jam, User]) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { + // decode uuid from URL + sid, err := s.parseUUID(r) + if err != nil { + s.Respond(w, r, sid, http.StatusBadRequest) + return + } + sub, err := b.GetSubscriber(sid) + if err != nil { + s.Respond(w, r, err, http.StatusNotFound) + return + } + + s.Respond(w, r, sub.Info, http.StatusOK) } } func (s *Service) handleListRooms(b *websocket.Broker[Jam, User]) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { + subs := b.ListSubscribers() + subsInfo := fp.FMap(subs, func(s *websocket.Subscriber[Jam, User]) Jam { + return *s.Info + }) + s.Respond(w, r, subsInfo, http.StatusOK) } } func (s *Service) handleP2PComms(b *websocket.Broker[Jam, User]) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // decode uuid from URL - sid, err := s.parseUUID(w, r) + sid, err := s.parseUUID(r) if err != nil { s.Respond(w, r, sid, http.StatusBadRequest) return @@ -173,6 +190,6 @@ func (s *Service) routes() { } -func (s *Service) parseUUID(w http.ResponseWriter, r *http.Request) (suid.UUID, error) { +func (s *Service) parseUUID(r *http.Request) (suid.UUID, error) { return suid.ParseString(chi.URLParam(r, "uuid")) } diff --git a/service/service.go b/service/service.go index 26d48f13..501ff4a8 100644 --- a/service/service.go +++ b/service/service.go @@ -8,7 +8,7 @@ import ( "github.com/go-chi/chi/middleware" "github.com/go-chi/chi/v5" "github.com/rog-golang-buddies/rmx/service/auth" - "github.com/rog-golang-buddies/rmx/service/jam" + jam "github.com/rog-golang-buddies/rmx/service/jam/v2" "github.com/rog-golang-buddies/rmx/store" ) From a46198b8e31b51015d2a3b94904248f96dfc6cb1 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Sun, 1 Jan 2023 01:17:39 +0330 Subject: [PATCH 243/246] subscriber connect method fix --- internal/websocket/subscriber.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/websocket/subscriber.go b/internal/websocket/subscriber.go index 9ee958f2..13afdf67 100644 --- a/internal/websocket/subscriber.go +++ b/internal/websocket/subscriber.go @@ -154,7 +154,12 @@ func (s *Subscriber[SI, CI]) connect(c *Conn[CI]) { defer c.lock.RUnlock() go func() { - defer c.rwc.Close() + defer func() { + if err := s.disconnect(c); err != nil { + s.errc <- &wsErr[CI]{c, err} + return + } + }() for { // read binary from connection From ef33bfc5e0998d5ce390325a5966f629114226d1 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Sun, 1 Jan 2023 18:28:46 +0330 Subject: [PATCH 244/246] new handlers --- internal/commands/run.go | 1 + internal/websocket/subscriber.go | 12 ++++++++++++ service/jam/v2/service.go | 26 ++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/internal/commands/run.go b/internal/commands/run.go index 3109bfc3..ba83f3bd 100644 --- a/internal/commands/run.go +++ b/internal/commands/run.go @@ -255,6 +255,7 @@ func serve(cfg *config.Config) error { AllowCredentials: true, AllowedMethods: []string{http.MethodGet, http.MethodPost}, AllowedHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + ExposedHeaders: []string{"Location"}, } // init application store diff --git a/internal/websocket/subscriber.go b/internal/websocket/subscriber.go index 13afdf67..0863c4f9 100644 --- a/internal/websocket/subscriber.go +++ b/internal/websocket/subscriber.go @@ -96,6 +96,18 @@ func (s *Subscriber[SI, CI]) Unsubscribe(c *Conn[CI]) error { // return s.disconnect(c) // } +func (s *Subscriber[SI, CI]) ListConns() []*Conn[CI] { + s.lock.RLock() + defer s.lock.RUnlock() + + conns := make([]*Conn[CI], 0, len(s.cs)) + for _, sub := range s.cs { + conns = append(conns, sub) + } + + return conns +} + func (s *Subscriber[SI, CI]) IsFull() bool { if s.Capacity == 0 { return false diff --git a/service/jam/v2/service.go b/service/jam/v2/service.go index 2d0e478f..8ac1aff8 100644 --- a/service/jam/v2/service.go +++ b/service/jam/v2/service.go @@ -130,6 +130,31 @@ func (s *Service) handleGetRoomData(b *websocket.Broker[Jam, User]) http.Handler } } +func (s *Service) handleGetRoomUsers(b *websocket.Broker[Jam, User]) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // decode uuid from URL + sid, err := s.parseUUID(r) + if err != nil { + s.Respond(w, r, sid, http.StatusBadRequest) + return + } + + sub, err := b.GetSubscriber(sid) + if err != nil { + s.Respond(w, r, err, http.StatusNotFound) + return + } + + conns := sub.ListConns() + + connsInfo := fp.FMap(conns, func(c *websocket.Conn[User]) User { + return *c.Info + }) + + s.Respond(w, r, connsInfo, http.StatusOK) + } +} + func (s *Service) handleListRooms(b *websocket.Broker[Jam, User]) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { subs := b.ListSubscribers() @@ -181,6 +206,7 @@ func (s *Service) routes() { s.Route("/api/v1/jam", func(r chi.Router) { r.Get("/", s.handleListRooms(broker)) r.Get("/{uuid}", s.handleGetRoomData(broker)) + r.Get("/{uuid}/users", s.handleGetRoomUsers(broker)) r.Post("/", s.handleCreateJamRoom(broker)) }) From bd639fb8b6343495235c64d35b4d487865627e3f Mon Sep 17 00:00:00 2001 From: Harvey Sanders Date: Sun, 1 Jan 2023 10:12:54 -0500 Subject: [PATCH 245/246] Export Jam ID for API responses --- service/jam/v2/service.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/service/jam/v2/service.go b/service/jam/v2/service.go index 8ac1aff8..f6f2562b 100644 --- a/service/jam/v2/service.go +++ b/service/jam/v2/service.go @@ -60,20 +60,25 @@ func (u *User) fillDefaults() { } type Jam struct { - id suid.UUID - owner *User - Name string `json:"name,omitempty"` - Capacity uint `json:"capacity,omitempty"` - BPM uint `json:"bpm,omitempty"` + // Unique Jam identifier. + ID suid.UUID `json:"id,omitempty"` + // Owning user of the Jam. + owner *User + // Public name of the Jam. + Name string `json:"name,omitempty"` + // Max number of Jam participants. + Capacity uint `json:"capacity,omitempty"` + // Beats per minute. Used for setting the tempo of MIDI playback. + BPM uint `json:"bpm,omitempty"` } func (j *Jam) fillDefaults() { - j.id = suid.NewUUID() + j.ID = suid.NewUUID() if j.owner == nil { - j.owner = &User{j.id, j.Name} + j.owner = &User{j.ID, j.Name} } if strings.TrimSpace(j.Name) == "" { - j.Name = j.id.ShortUUID().String() + j.Name = j.ID.ShortUUID().String() } if j.Capacity == 0 { j.Capacity = 10 From 1c2eb0b34ff63afdd8cde6840eca49d705c93357 Mon Sep 17 00:00:00 2001 From: pmoieni Date: Sun, 1 Jan 2023 19:11:18 +0330 Subject: [PATCH 246/246] send Jam IDs --- service/jam/v2/service.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/service/jam/v2/service.go b/service/jam/v2/service.go index f6f2562b..82fcf779 100644 --- a/service/jam/v2/service.go +++ b/service/jam/v2/service.go @@ -48,24 +48,20 @@ const ( ) type User struct { - id suid.UUID Username string `json:"username"` } func (u *User) fillDefaults() { - u.id = suid.NewUUID() if strings.TrimSpace(u.Username) == "" { - u.Username = u.id.String() + u.Username = "User-" + suid.NewSUID().String() } } type Jam struct { - // Unique Jam identifier. - ID suid.UUID `json:"id,omitempty"` - // Owning user of the Jam. - owner *User // Public name of the Jam. Name string `json:"name,omitempty"` + // Owning user of the Jam. (not implemented yet) + Owner *User `json:"owner,omitempty"` // Max number of Jam participants. Capacity uint `json:"capacity,omitempty"` // Beats per minute. Used for setting the tempo of MIDI playback. @@ -73,12 +69,8 @@ type Jam struct { } func (j *Jam) fillDefaults() { - j.ID = suid.NewUUID() - if j.owner == nil { - j.owner = &User{j.ID, j.Name} - } if strings.TrimSpace(j.Name) == "" { - j.Name = j.ID.ShortUUID().String() + j.Name = "Jam-" + suid.NewSUID().String() } if j.Capacity == 0 { j.Capacity = 10 @@ -161,10 +153,18 @@ func (s *Service) handleGetRoomUsers(b *websocket.Broker[Jam, User]) http.Handle } func (s *Service) handleListRooms(b *websocket.Broker[Jam, User]) http.HandlerFunc { + type response struct { + ID suid.SUID `json:"id"` + Jam + } + return func(w http.ResponseWriter, r *http.Request) { subs := b.ListSubscribers() - subsInfo := fp.FMap(subs, func(s *websocket.Subscriber[Jam, User]) Jam { - return *s.Info + subsInfo := fp.FMap(subs, func(s *websocket.Subscriber[Jam, User]) *response { + return &response{ + ID: s.GetID().ShortUUID(), + Jam: *s.Info, + } }) s.Respond(w, r, subsInfo, http.StatusOK)