diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..1b7c07b08 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,36 @@ +name: E2E + +on: + workflow_dispatch: + pull_request: + branches: + - develop + paths: + - ".github/workflows/e2e.yml" + - "Makefile" + - "compose.e2e.yml" + - "api/**" + - "mysql/**" + - "openapi/**" + - "view/**" + - "my.cnf" + +concurrency: + group: e2e-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + signup-e2e: + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run E2E + run: make run-e2e + + - name: Cleanup E2E containers + if: always() + run: docker compose -f compose.e2e.yml down --volumes --remove-orphans diff --git a/Makefile b/Makefile index f951177aa..2762d8a61 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # アプリコンテナ=view,api、DBコンテナ=db,minio -include finansu.env +-include finansu.env # 配色 SHELL := /bin/bash @@ -216,6 +216,12 @@ run-test: ## APIテスト実行 run-eslint: ## ESLint実行 docker compose exec view pnpm run lint +run-e2e: ## E2Eテスト実行 (DB/API/View/PlaywrightをDocker内で完結) + docker compose -f compose.e2e.yml down --volumes --remove-orphans + docker compose -f compose.e2e.yml up --build -d db minio migrate seed api view + docker compose -f compose.e2e.yml run --rm --no-deps e2e + docker compose -f compose.e2e.yml down --volumes --remove-orphans + ##@ クリーンアップ del-vol: ## アプリコンテナボリューム削除 docker compose down -v diff --git a/api/drivers/server/server.go b/api/drivers/server/server.go index 7d06b625e..5e1854488 100644 --- a/api/drivers/server/server.go +++ b/api/drivers/server/server.go @@ -8,6 +8,7 @@ import ( "github.com/NUTFes/FinanSu/api/generated" echo "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + oapimiddleware "github.com/oapi-codegen/echo-middleware" ) func RunServer(server *handler.Handler) *echo.Echo { @@ -28,10 +29,23 @@ func RunServer(server *handler.Handler) *echo.Echo { // CORS対策 e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ - AllowOrigins: []string{"http://localhost:3000", "127.0.0.1:3000", "http://localhost:3001", "127.0.0.1:3001", "http://localhost:8000", "127.0.0.1:8000", "https://finansu.nutfes.net", "https://stg-finansu.nutfes.net"}, // ドメイン + AllowOrigins: []string{"http://localhost:3000", "127.0.0.1:3000", "http://view:3000", "http://localhost:3001", "127.0.0.1:3001", "http://localhost:8000", "127.0.0.1:8000", "https://finansu.nutfes.net", "https://stg-finansu.nutfes.net"}, // ドメイン AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}, })) + swagger, err := generated.GetSwagger() + if err != nil { + panic(err) + } + swagger.Servers = nil + e.Use(oapimiddleware.OapiRequestValidatorWithOptions(swagger, &oapimiddleware.Options{ + Skipper: func(c echo.Context) bool { + // TODO: OpenAPI定義と既存APIの実装差分を解消し、全体にvalidatorを適用する。 + // 現状は全APIへ適用すると既存エンドポイントに影響が出るため、signupだけを検証対象にしている。 + return c.Path() != "/mail_auth/signup" || c.Request().Method != http.MethodPost + }, + })) + // ルーティング generated.RegisterHandlers(e, server) diff --git a/api/externals/handler/mail_auth_handler.go b/api/externals/handler/mail_auth_handler.go index 682b50c99..cf3cb97d5 100644 --- a/api/externals/handler/mail_auth_handler.go +++ b/api/externals/handler/mail_auth_handler.go @@ -45,11 +45,18 @@ func (h *Handler) DeleteMailAuthSignout(c echo.Context, params generated.DeleteM } // router.POST(baseURL+"/mail_auth/signup", wrapper.PostMailAuthSignup) -func (h *Handler) PostMailAuthSignup(c echo.Context, params generated.PostMailAuthSignupParams) error { - email := params.Email - password := params.Password - userID := strconv.Itoa(params.UserId) - token, err := h.mailAuthUseCase.SignUp(c.Request().Context(), email, password, userID) +func (h *Handler) PostMailAuthSignup(c echo.Context) error { + var request generated.PostMailAuthSignupJSONRequestBody + if err := c.Bind(&request); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid request body") + } + + email := request.Email + password := request.Password + name := request.Name + bureauID := strconv.Itoa(request.BureauId) + roleID := strconv.Itoa(request.RoleId) + token, err := h.mailAuthUseCase.SignUp(c.Request().Context(), email, password, name, bureauID, roleID) if err != nil { return err } diff --git a/api/externals/repository/mail_auth_repository.go b/api/externals/repository/mail_auth_repository.go index 8533a2ed9..48f1a8dc1 100644 --- a/api/externals/repository/mail_auth_repository.go +++ b/api/externals/repository/mail_auth_repository.go @@ -3,10 +3,11 @@ package repository import ( "context" "database/sql" - "fmt" "github.com/NUTFes/FinanSu/api/drivers/db" "github.com/NUTFes/FinanSu/api/externals/repository/abstract" + goqu "github.com/doug-martin/goqu/v9" + "github.com/doug-martin/goqu/v9/exp" ) type mailAuthRepository struct { @@ -16,9 +17,12 @@ type mailAuthRepository struct { type MailAuthRepository interface { CreateMailAuth(context.Context, string, string, string) (int64, error) - FindMailAuthByEmail(context.Context, string) *sql.Row - FindMailAuthByID(context.Context, string) *sql.Row + CreateMailAuthWithTx(context.Context, *sql.Tx, string, string, string) (int64, error) + FindMailAuthByEmail(context.Context, string) (*sql.Row, error) + FindMailAuthByID(context.Context, string) (*sql.Row, error) ChangePasswordByUserID(context.Context, string, string) error + InvalidateEmailByUserIDWithTx(context.Context, *sql.Tx, string) error + InvalidateEmailByUserIDsWithTx(context.Context, *sql.Tx, []int) error } func NewMailAuthRepository(client db.Client, crud abstract.Crud) MailAuthRepository { @@ -27,7 +31,12 @@ func NewMailAuthRepository(client db.Client, crud abstract.Crud) MailAuthReposit // 作成 func (r *mailAuthRepository) CreateMailAuth(c context.Context, email string, password string, userID string) (int64, error) { - result, err := r.client.DB().ExecContext(c, "insert into mail_auth (email, password, user_id) values ('"+email+"','"+password+"',"+userID+")") + query, args, err := createMailAuthQuery(email, password, userID) + if err != nil { + return 0, err + } + + result, err := r.client.DB().ExecContext(c, query, args...) if err != nil { return 0, err } @@ -35,24 +44,105 @@ func (r *mailAuthRepository) CreateMailAuth(c context.Context, email string, pas return lastInsertID, err } +func (r *mailAuthRepository) CreateMailAuthWithTx(c context.Context, tx *sql.Tx, email string, password string, userID string) (int64, error) { + query, args, err := createMailAuthQuery(email, password, userID) + if err != nil { + return 0, err + } + + result, err := tx.ExecContext(c, query, args...) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + // メールアドレスからmail_authを探してくる -func (r *mailAuthRepository) FindMailAuthByEmail(c context.Context, email string) *sql.Row { - query := "select * from mail_auth where email= '" + email + "'" - row := r.client.DB().QueryRowContext(c, query) - fmt.Printf("\x1b[36m%s\n", query) - return row +func (r *mailAuthRepository) FindMailAuthByEmail(c context.Context, email string) (*sql.Row, error) { + query, args, err := dialect.From("mail_auth"). + Prepared(true). + Where(goqu.Ex{"email": email}). + ToSQL() + if err != nil { + return nil, err + } + + return r.client.DB().QueryRowContext(c, query, args...), nil } // mail_auth_idからmail_authを探してくる -func (r *mailAuthRepository) FindMailAuthByID(c context.Context, id string) *sql.Row { - query := "select * from mail_auth where id= " + id - row := r.client.DB().QueryRowContext(c, query) - fmt.Printf("\x1b[36m%s\n", query) - return row +func (r *mailAuthRepository) FindMailAuthByID(c context.Context, id string) (*sql.Row, error) { + query, args, err := dialect.From("mail_auth"). + Prepared(true). + Where(goqu.Ex{"id": id}). + ToSQL() + if err != nil { + return nil, err + } + + return r.client.DB().QueryRowContext(c, query, args...), nil } // パスワードの変更 func (r *mailAuthRepository) ChangePasswordByUserID(c context.Context, userID string, password string) error { - query := "UPDATE mail_auth SET password = '" + password + "' WHERE user_id = " + userID - return r.crud.UpdateDB(c, query) + query, args, err := updateMailAuthQuery( + goqu.Record{"password": password}, + goqu.Ex{"user_id": userID}, + ) + if err != nil { + return err + } + + _, err = r.client.DB().ExecContext(c, query, args...) + return err +} + +func (r *mailAuthRepository) InvalidateEmailByUserIDWithTx(c context.Context, tx *sql.Tx, userID string) error { + query, args, err := updateMailAuthQuery( + goqu.Record{"email": nil}, + goqu.Ex{"user_id": userID}, + ) + if err != nil { + return err + } + + _, err = tx.ExecContext(c, query, args...) + return err +} + +func (r *mailAuthRepository) InvalidateEmailByUserIDsWithTx(c context.Context, tx *sql.Tx, userIDs []int) error { + if len(userIDs) == 0 { + return nil + } + + query, args, err := updateMailAuthQuery( + goqu.Record{"email": nil}, + goqu.I("user_id").In(userIDs), + ) + if err != nil { + return err + } + + _, err = tx.ExecContext(c, query, args...) + return err +} + +func createMailAuthQuery(email string, password string, userID string) (string, []any, error) { + return dialect.Insert("mail_auth"). + Prepared(true). + Rows(goqu.Record{ + "email": email, + "password": password, + "user_id": userID, + }). + ToSQL() +} + +func updateMailAuthQuery(record goqu.Record, first exp.Expression, rest ...exp.Expression) (string, []any, error) { + where := append([]exp.Expression{first}, rest...) + return dialect.Update("mail_auth"). + Prepared(true). + Set(record). + Where(where...). + ToSQL() } diff --git a/api/externals/repository/session_repository.go b/api/externals/repository/session_repository.go index e53386846..2ed472653 100644 --- a/api/externals/repository/session_repository.go +++ b/api/externals/repository/session_repository.go @@ -3,20 +3,24 @@ package repository import ( "context" "database/sql" + "github.com/NUTFes/FinanSu/api/drivers/db" - "fmt" + goqu "github.com/doug-martin/goqu/v9" + "github.com/doug-martin/goqu/v9/exp" ) - type sessionRepository struct { client db.Client } type SessionRepository interface { Create(context.Context, string, string, string) error + CreateWithTx(context.Context, *sql.Tx, string, string, string) error Destroy(context.Context, string) error - FindSessionByAccessToken(context.Context, string) *sql.Row + FindSessionByAccessToken(context.Context, string) (*sql.Row, error) DestroyByUserID(context.Context, string) error + DestroyByUserIDWithTx(context.Context, *sql.Tx, string) error + DestroyByUserIDsWithTx(context.Context, *sql.Tx, []int) error } func NewSessionRepository(client db.Client) SessionRepository { @@ -25,42 +29,112 @@ func NewSessionRepository(client db.Client) SessionRepository { // 作成 func (r *sessionRepository) Create(c context.Context, authID string, userID string, accessToken string) error { - query := "insert into session (auth_id, user_id, access_token) values (" + authID + ", " + userID + ", '" + accessToken + "')" - _, err := r.client.DB().ExecContext(c, query) + query, args, err := createSessionQuery(authID, userID, accessToken) + if err != nil { + return err + } + + _, err = r.client.DB().ExecContext(c, query, args...) + if err != nil { + return err + } + return nil +} + +func (r *sessionRepository) CreateWithTx(c context.Context, tx *sql.Tx, authID string, userID string, accessToken string) error { + query, args, err := createSessionQuery(authID, userID, accessToken) + if err != nil { + return err + } + + _, err = tx.ExecContext(c, query, args...) if err != nil { return err } - fmt.Printf("\x1b[36m%s\n", query) return nil } // 削除 func (r *sessionRepository) Destroy(c context.Context, accessToken string) error { // access tokenで該当のsessionを削除 - query := "delete from session where access_token = '" + accessToken + "'" - _, err := r.client.DB().ExecContext(c, query) + query, args, err := deleteSessionQuery(goqu.Ex{"access_token": accessToken}) + if err != nil { + return err + } + + _, err = r.client.DB().ExecContext(c, query, args...) if err != nil { return err } - fmt.Printf("\x1b[36m%s\n", query) return nil } // アクセストークンからセッションを取得 -func (r *sessionRepository) FindSessionByAccessToken(c context.Context, accessToken string) *sql.Row { - query := "select * from session where access_token = '" + accessToken + "'" - row := r.client.DB().QueryRowContext(c, query) - fmt.Printf("\x1b[36m%s\n", query) - return row +func (r *sessionRepository) FindSessionByAccessToken(c context.Context, accessToken string) (*sql.Row, error) { + query, args, err := dialect.From("session"). + Prepared(true). + Where(goqu.Ex{"access_token": accessToken}). + ToSQL() + if err != nil { + return nil, err + } + + return r.client.DB().QueryRowContext(c, query, args...), nil } // user_idからsessionを削除する func (r *sessionRepository) DestroyByUserID(c context.Context, userID string) error { - query := "delete from session where user_id = " + userID - _, err := r.client.DB().ExecContext(c, query) + query, args, err := deleteSessionQuery(goqu.Ex{"user_id": userID}) + if err != nil { + return err + } + + _, err = r.client.DB().ExecContext(c, query, args...) if err != nil { return err } - fmt.Printf("\x1b[36m%s\n", query) return nil } + +func (r *sessionRepository) DestroyByUserIDWithTx(c context.Context, tx *sql.Tx, userID string) error { + query, args, err := deleteSessionQuery(goqu.Ex{"user_id": userID}) + if err != nil { + return err + } + + _, err = tx.ExecContext(c, query, args...) + return err +} + +func (r *sessionRepository) DestroyByUserIDsWithTx(c context.Context, tx *sql.Tx, userIDs []int) error { + if len(userIDs) == 0 { + return nil + } + + query, args, err := deleteSessionQuery(goqu.I("user_id").In(userIDs)) + if err != nil { + return err + } + + _, err = tx.ExecContext(c, query, args...) + return err +} + +func createSessionQuery(authID string, userID string, accessToken string) (string, []any, error) { + return dialect.Insert("session"). + Prepared(true). + Rows(goqu.Record{ + "auth_id": authID, + "user_id": userID, + "access_token": accessToken, + }). + ToSQL() +} + +func deleteSessionQuery(first exp.Expression, rest ...exp.Expression) (string, []any, error) { + where := append([]exp.Expression{first}, rest...) + return dialect.Delete("session"). + Prepared(true). + Where(where...). + ToSQL() +} diff --git a/api/externals/repository/user_repository.go b/api/externals/repository/user_repository.go index 9ffb13c28..8a30134ba 100644 --- a/api/externals/repository/user_repository.go +++ b/api/externals/repository/user_repository.go @@ -3,11 +3,11 @@ package repository import ( "context" "database/sql" - "strconv" "github.com/NUTFes/FinanSu/api/drivers/db" "github.com/NUTFes/FinanSu/api/externals/repository/abstract" goqu "github.com/doug-martin/goqu/v9" + "github.com/doug-martin/goqu/v9/exp" ) type userRepository struct { @@ -19,10 +19,11 @@ type UserRepository interface { All(context.Context) (*sql.Rows, error) Find(context.Context, string) (*sql.Row, error) FindByIDs(context.Context, []int) (*sql.Rows, error) - Create(context.Context, string, string, string) error + Create(context.Context, string, string, string) (int64, error) + CreateWithTx(context.Context, *sql.Tx, string, string, string) (int64, error) Update(context.Context, string, string, string, string) error - Destroy(context.Context, string) error - MultiDestroy(context.Context, []int) error + DestroyWithTx(context.Context, *sql.Tx, string) error + MultiDestroyWithTx(context.Context, *sql.Tx, []int) error FindNewRecord(context.Context) (*sql.Row, error) FindByEmail(context.Context, string) (*sql.Row, error) } @@ -33,100 +34,168 @@ func NewUserRepository(c db.Client, ac abstract.Crud) UserRepository { // 全件取得 func (ur *userRepository) All(c context.Context) (*sql.Rows, error) { - query := "SELECT * FROM users WHERE is_deleted IS FALSE" - return ur.crud.Read(c, query) + query, args, err := dialect.From("users"). + Prepared(true). + Where(goqu.Ex{"is_deleted": false}). + ToSQL() + if err != nil { + return nil, err + } + + return ur.client.DB().QueryContext(c, query, args...) } // 1件取得 func (ur *userRepository) Find(c context.Context, id string) (*sql.Row, error) { - query := "SELECT * FROM users WHERE id = " + id - return ur.crud.ReadByID(c, query) + query, args, err := dialect.From("users"). + Prepared(true). + Where(goqu.Ex{"id": id}). + ToSQL() + if err != nil { + return nil, err + } + + return ur.client.DB().QueryRowContext(c, query, args...), nil } // 複数件取得 func (ur *userRepository) FindByIDs(c context.Context, ids []int) (*sql.Rows, error) { ds := dialect. From("users"). + Prepared(true). Select("users.*"). Where(goqu.I("users.id").In(ids)) - query, _, err := ds.ToSQL() + query, args, err := ds.ToSQL() if err != nil { return nil, err } - return ur.crud.Read(c, query) + return ur.client.DB().QueryContext(c, query, args...) } // 作成 -func (ur *userRepository) Create(c context.Context, name string, bureauID string, roleID string) error { - query := ` - INSERT INTO - users (name, bureau_id, role_id) - VALUES ('` + name + "', " + bureauID + ", " + roleID + ")" - return ur.crud.UpdateDB(c, query) -} +func (ur *userRepository) Create(c context.Context, name string, bureauID string, roleID string) (int64, error) { + query, args, err := createUserQuery(name, bureauID, roleID) + if err != nil { + return 0, err + } -// 編集 -func (ur *userRepository) Update(c context.Context, id string, name string, bureauID string, roleID string) error { - query := ` - UPDATE - users - SET - name = '` + name + - "', bureau_id = " + bureauID + - ", role_id = " + roleID + - " WHERE id = " + id - return ur.crud.UpdateDB(c, query) + result, err := ur.client.DB().ExecContext(c, query, args...) + if err != nil { + return 0, err + } + return result.LastInsertId() } -// 削除 -func (ur *userRepository) Destroy(c context.Context, id string) error { - query := "UPDATE users SET is_deleted = TRUE WHERE id =" + id +func (ur *userRepository) CreateWithTx(c context.Context, tx *sql.Tx, name string, bureauID string, roleID string) (int64, error) { + query, args, err := createUserQuery(name, bureauID, roleID) + if err != nil { + return 0, err + } - err := ur.crud.UpdateDB(c, query) + result, err := tx.ExecContext(c, query, args...) if err != nil { - return err + return 0, err } + return result.LastInsertId() +} - query = "UPDATE mail_auth SET email = NULL WHERE user_id =" + id - err = ur.crud.UpdateDB(c, query) +// 編集 +func (ur *userRepository) Update(c context.Context, id string, name string, bureauID string, roleID string) error { + query, args, err := updateUserQuery( + goqu.Record{ + "name": name, + "bureau_id": bureauID, + "role_id": roleID, + }, + goqu.Ex{"id": id}, + ) + if err != nil { + return err + } + _, err = ur.client.DB().ExecContext(c, query, args...) return err } -// 複数削除 -func (ur *userRepository) MultiDestroy(c context.Context, ids []int) error { - query := "UPDATE users SET is_deleted = TRUE WHERE " - query2 := "UPDATE mail_auth SET email = NULL WHERE " - for index, id := range ids { - query += "id = " + strconv.Itoa(id) - query2 += "user_id = " + strconv.Itoa(id) +func (ur *userRepository) DestroyWithTx(c context.Context, tx *sql.Tx, id string) error { + userQuery, userArgs, err := updateUserQuery( + goqu.Record{"is_deleted": true}, + goqu.Ex{"id": id}, + ) + if err != nil { + return err + } - if index != len(ids)-1 { - query += " OR " - query2 += " OR " - } + _, err = tx.ExecContext(c, userQuery, userArgs...) + return err +} +func (ur *userRepository) MultiDestroyWithTx(c context.Context, tx *sql.Tx, ids []int) error { + if len(ids) == 0 { + return nil } - err := ur.crud.UpdateDB(c, query) + userQuery, userArgs, err := updateUserQuery( + goqu.Record{"is_deleted": true}, + goqu.I("id").In(ids), + ) if err != nil { return err } - err = ur.crud.UpdateDB(c, query2) - + _, err = tx.ExecContext(c, userQuery, userArgs...) return err } func (ur *userRepository) FindNewRecord(c context.Context) (*sql.Row, error) { - query := "SELECT * FROM users WHERE is_deleted IS FALSE ORDER BY id DESC LIMIT 1" - return ur.crud.ReadByID(c, query) + query, args, err := dialect.From("users"). + Prepared(true). + Where(goqu.Ex{"is_deleted": false}). + Order(goqu.I("id").Desc()). + Limit(1). + ToSQL() + if err != nil { + return nil, err + } + + return ur.client.DB().QueryRowContext(c, query, args...), nil } // 1件取得 func (ur *userRepository) FindByEmail(c context.Context, email string) (*sql.Row, error) { - query := "SELECT * FROM users INNER JOIN mail_auth ON users.id = mail_auth.user_id WHERE is_deleted IS FALSE AND email = '" + email + "'" - return ur.crud.ReadByID(c, query) + query, args, err := dialect.From("users"). + Prepared(true). + Join(goqu.I("mail_auth"), goqu.On(goqu.I("users.id").Eq(goqu.I("mail_auth.user_id")))). + Where( + goqu.Ex{"users.is_deleted": false}, + goqu.Ex{"mail_auth.email": email}, + ). + ToSQL() + if err != nil { + return nil, err + } + + return ur.client.DB().QueryRowContext(c, query, args...), nil +} + +func createUserQuery(name string, bureauID string, roleID string) (string, []any, error) { + return dialect.Insert("users"). + Prepared(true). + Rows(goqu.Record{ + "name": name, + "bureau_id": bureauID, + "role_id": roleID, + }). + ToSQL() +} + +func updateUserQuery(record goqu.Record, first exp.Expression, rest ...exp.Expression) (string, []any, error) { + where := append([]exp.Expression{first}, rest...) + return dialect.Update("users"). + Prepared(true). + Set(record). + Where(where...). + ToSQL() } diff --git a/api/generated/openapi_gen.go b/api/generated/openapi_gen.go index 424ee8893..763d189b8 100644 --- a/api/generated/openapi_gen.go +++ b/api/generated/openapi_gen.go @@ -4,10 +4,17 @@ package generated import ( + "bytes" + "compress/gzip" + "encoding/base64" "fmt" "net/http" + "net/url" + "path" + "strings" "time" + "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" "github.com/oapi-codegen/runtime" openapi_types "github.com/oapi-codegen/runtime/types" @@ -816,16 +823,22 @@ type DeleteMailAuthSignoutParams struct { AccessToken *string `json:"Access-Token,omitempty"` } -// PostMailAuthSignupParams defines parameters for PostMailAuthSignup. -type PostMailAuthSignupParams struct { +// PostMailAuthSignupJSONBody defines parameters for PostMailAuthSignup. +type PostMailAuthSignupJSONBody struct { + // BureauId bureau_id + BureauId int `json:"bureau_id"` + // Email email - Email string `form:"email" json:"email"` + Email string `json:"email"` + + // Name name + Name string `json:"name"` // Password password - Password string `form:"password" json:"password"` + Password string `json:"password"` - // UserId user_id - UserId int `form:"user_id" json:"user_id"` + // RoleId role_id + RoleId int `json:"role_id"` } // PostPasswordResetRequestParams defines parameters for PostPasswordResetRequest. @@ -1035,6 +1048,9 @@ type PostIncomesJSONRequestBody = Income // PutIncomesIdJSONRequestBody defines body for PutIncomesId for application/json ContentType. type PutIncomesIdJSONRequestBody = Income +// PostMailAuthSignupJSONRequestBody defines body for PostMailAuthSignup for application/json ContentType. +type PostMailAuthSignupJSONRequestBody PostMailAuthSignupJSONBody + // PostPasswordResetIdJSONRequestBody defines body for PostPasswordResetId for application/json ContentType. type PostPasswordResetIdJSONRequestBody = PasswordResetData @@ -1300,7 +1316,7 @@ type ServerInterface interface { DeleteMailAuthSignout(ctx echo.Context, params DeleteMailAuthSignoutParams) error // (POST /mail_auth/signup) - PostMailAuthSignup(ctx echo.Context, params PostMailAuthSignupParams) error + PostMailAuthSignup(ctx echo.Context) error // (POST /password_reset/request) PostPasswordResetRequest(ctx echo.Context, params PostPasswordResetRequestParams) error @@ -2762,31 +2778,8 @@ func (w *ServerInterfaceWrapper) DeleteMailAuthSignout(ctx echo.Context) error { func (w *ServerInterfaceWrapper) PostMailAuthSignup(ctx echo.Context) error { var err error - // Parameter object where we will unmarshal all parameters from the context - var params PostMailAuthSignupParams - // ------------- Required query parameter "email" ------------- - - err = runtime.BindQueryParameter("form", true, true, "email", ctx.QueryParams(), ¶ms.Email) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter email: %s", err)) - } - - // ------------- Required query parameter "password" ------------- - - err = runtime.BindQueryParameter("form", true, true, "password", ctx.QueryParams(), ¶ms.Password) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter password: %s", err)) - } - - // ------------- Required query parameter "user_id" ------------- - - err = runtime.BindQueryParameter("form", true, true, "user_id", ctx.QueryParams(), ¶ms.UserId) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter user_id: %s", err)) - } - // Invoke the callback with all the unmarshaled arguments - err = w.Handler.PostMailAuthSignup(ctx, params) + err = w.Handler.PostMailAuthSignup(ctx) return err } @@ -3830,3 +3823,239 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.PUT(baseURL+"/years/:id", wrapper.PutYearsId) } + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "H4sIAAAAAAAC/+x9a28bR5boXxF474fMgjIpZbLY1cUC17HWA2F2ZozYzsXduQbRIktSr8luTnfTEyEw", + "oCb9kCM7kp9axZ6xlTi2HmPKsTV5WLLzX6bUJPXJf+Giq/pRXV1VXRQfkhkPBoFtVledOu9zquqcz1N5", + "vVTWNaBZZmrs85SZnwElBf3xeN5SL6jW7Omyrpm6cdqaLYL/ULXz7m8FYOYNtWypupYaSzk37rRe3m9s", + "7zgLd6G9BmvLsLYOay+hXW9uL+3t/De0bzZql51H36XSqbKhl4FhqQAtklcsMK0bs+6f/6cBplJjqf+R", + "CUHKePBkSCBO+N9cTKfUgvsl+EwplYsgNTaSTaes2TJIjaVUzQLTwHAHmcTHE9EPRpnj3YFJEJGTpi5e", + "DObRJ/8L5C13mgCBlmJVzDjaPIRVf4K1K7C2C6s/w+pPqXQKaJVSauyPqYpmWophgUIqnSopFjBUpZgz", + "gWal0qkp3SiZ/l/yujalGiU0UNUu6Goe+D+VldkS0KwcOcQAeaCWLX+IAVyAQSF1LtiDaRmqNu3u4YQB", + "FAt42Ddn1LK/qU/AnyrAtBi7uve89WRx7/WDxvxSY6UK7TqsbcDqFqyuoa3Ow9oDWLsKq9/EmEGJIUxE", + "AAq9F9MuIOq0dsrQpw1gJn4/Hh19MZ2aAoqpTqpFaRBOxj646OKzpBjnzQibpZrXfoL2JrQ3oL2cYuDZ", + "46YJmp1Hkvh5HFiKWmRwV3NlZ//6C2gvQ/shtC9hGQ1EE4ujTxyXLC7vWKCEZuqujArEj7E9liR5/6AY", + "hoJmrJiARtVHLETNAsU4BQxVL5jJwxHp/lRRDVBwpS/6LUmhYP00zbEsFoqx5TnGBsdjnBslJhKY72H1", + "May93J970bixzNETeSSvhdzkbM60KgVPP4T/6JJN0WaR0nAxwZP7kyxRYGl9Z3HLWXrCgaasm6Y6WQQu", + "b5WCv7DWY/IOZ0mXX11d+RjWNl0Orm66CrS67bJybfft7jy0v9i/ehPaC81r685tG9oLb3evESCWdA24", + "KJjW9YIpAodQeCowPwGIB4DYAO79ONd68hRJ1t+QZP3FtYRIsTO1nfe3QPYkJIzUwyzxsHRLKR4v6RWN", + "oZ9b6y+c17ddhsImx65j6Pev3txfveEszbfWXF0QSMqHH2Wz2aycnLLgE3sLBBit9ZfN7eccP6FT0+CJ", + "wHErqpZHs6P/PJz9aDg7cib7r2PZ7Fg2+5+edVWs1FiqoFhg2FJLgKWzj4a5URM1KsckwdpNlzNrd11G", + "rdVcy2yv+RJT33v99+Yd0pV7Cu0VgeWSdJc6snUsK7e9BO1vob1I2TeXra9cbq29ZNo3Kd6h3V6WHSoX", + "RFw1mj0zkm2Xq1zbkgQjGtNDOxgT7LNoo225gfe3G/eev3cAfZ7A+EDu2BvslPXHDSTWlXID3+7OO5fX", + "mq/rjcX72Gq+9woPyyvkSh2ehCt7VEj5XhSjorh/f6e1/h0Kx540v37V2rjR+HEe2j/HBZIifrfoqhC+", + "URTr+OMIuMyMBvisDDxPVDxwCihWxQCU5eeGoao5rmvR0VNK0QTB0EldLwJFC3ErO7MvKOPJ3krFKEZn", + "5ZnJ5MkoAvpoC/ESbDnYDwlqsIyIijlVw5YdCd/nHDmS2fhkJX8eWL9XStL0ikuZeIUptQgmouCK0ex+", + "0A5A7vgz6B9lxl8UoTXIwh0coREb0y6zEAvFZmLxw2RFLRZUbfqMG30xDPE3D6F9B9prrmuKoixn4SEO", + "umLKd9rQK+XfgkTbmldK5YrLv4iaH3sA/Mb/WjI60GL03b+/03i+3PiG6aOg6PKUoeajH42M8sJEEqmq", + "ax/Rgulwl5E52aid/QSUdYMVzr7cdS5/6zz6zrn5BbTr+1/dR1buJvJvVrENjBu3IDYmfKwsR4GalnpB", + "KU5YoCTDcjIILytq4ePZKMr3du3mnR3nu7lc44s55/FT5/HGfu1LFv7x12fjzk4yS1ObSfuIEOIcu5YJ", + "mK99hVD9I7SftlbXmo9fQXsFVhfEMb08FQrqBdVUdY2himq3UX5le7+2tn9vjqmTiF3HJ2jU/468oF1Y", + "25TXgBYwrWNlbZr3xSnFmol+kRnNjn6U8XLvZuakPy1zAk3R8qpS/ATkdaMQXz1gFqYNl+BA1Tyl5M+D", + "gpylV83TwLKKssP7x94udyMWVSwQC8F/PZwdQYkdIgSPreb689EDoexochTgcS7FlwxOY9MyjdVgQAQS", + "wwH6InsjmEooq5Rtp5PIf0VJU46oRhVp5zIbANW+zKl9oLwZRBV+WhjHAns/PkulU85zu7X+HFbtVDrV", + "+PFys77s1K/vvbrCyBZfFBHkdKVUUlipbBrdDzaaL94068uZxoONYPEgGxslQkUrI8bhpXh9xvo3JKLQ", + "rmOqxXO7/8IhXUUzMTvyV/D4NXEJObeAXjBN71HI9f9HtWbGPUnESqN9HwHO2diCBEnQiXFYveUsbcLq", + "XDdMl4w2e+edjRiNoh5q3JtXGeRynj1xrlzG3jHyUhI26Lui1CzYu/YdbELBs+mELPMFP5lLzbW4vP/o", + "vxvL35ITeWneXw+P/gud3GW6zUDJz/hoZIHqbL1pfbfq6oK7K87tVYmdhwkv1nywess7A67tRLKAtSeI", + "5b+HtV2JRVwLyVzkp23n1ZPECVj+f5AoC3ESrOPTM0KQc4mc5cc+J4s6PouggF265Cz+gEKweuP6Vaf+", + "FTr3eAarX2Nsc8ydH9PJCK8/lmHu1h82vn4OazvOzqvW2jNOXFXwtiJ/HBhFwRmMTFZmdMrFyu8rpUkQ", + "dXZSHzFTK5pqMQYnE5fAFoWOyKRReMiNy9P5N0SEzNcelIvj0r/6HNY2keexjM6On8HaLnk0DPIziqbm", + "lWIur15QizmgTasaAAg36RQogrxloJ+jP0yqelGfnkUzXVANXSsBzVKKOXPWtECJuMBj5kqKpkwDdCuH", + "zBylU9NAA4ZSzBnABIqRn3Ej5Uq+CBTDmyZnKlPAchdRCiVVU03L8L8Fn1kGKIGcO8X0bK4ANFO1ZoOp", + "cnmgWQjZJSU/o2ogZ87o5VQ6VVSMaZADn5WBoZbwbQFFU4qzpmrmVM20jAraiLtKOIduuXzGOjePEotI", + "UEfF6r3KfodUdg+0ta+qmEmy2s7+V18GqTKMWWf+W4wh7jVCcnrm3hGpMAagvYnRjPP+0K4LXQ6tUiwq", + "k+5fLKMC2OH0x0Ulf14yPI6lzj5C/J28jKHrjATGR9mRROYV84s3lGG4hN4hxScBdFFOISdPxwkVcpCP", + "RBb7FIBpGfqsxzcT4ybr6KQILOD9FGzhj+eIA0xvWg0bnpidpPYTTiiACHnGRwEcL8qIA0IlILoVVsST", + "xq3V+807D5v11ebSFV4yjtqTxsyRcPLr/haJo24K5d4AeQfK/8INID9WioqWB9z7VElz4UHMIMhf5g9l", + "dhDUTojYTbwT64oQTmInBvtk+EMA0r9yM0KFaRCPmqVPOLk5+ij/UDdvf1h0FlY6yJUeGOMxhJLhPeNg", + "i51T6DipILPHEijpyYeBDEy8eYEuOG4cgOWEpw8kqrgCTw6SF3ryqx4LPrkUT/gjOZ+DsaM0Eai1vMmS", + "8H8o8k8atPYk7yjpjt7IVSK1wsNSOp3hZUzlpYV5tMBKMQgP2YJTusbKlnPbTjqlO9NFuTNP6sbvZk8p", + "04BveA90rOh/3A6w6S7pLI/ALDp0cHbIRGVcPlgZ3HZ1llCy3OAyp7Z7uO1ZGP/jc8mb4RuW6Lg26BT9", + "sNfmhb+atJ5uS1G35alluxdaJHILfYT7a7nTCVXL6yXQmSsmddyO1pE7z0ZplNBZ8BOTM4qGomlD0cwp", + "TvbNu6TElPnGt89GeNibOKCoBcfgQTYo2Og5LrbJ1z3d0CSNuyut6ovG9a3W/Pze7letFztyvigfwn93", + "GbqgWhUD/C7I1cozCZNL8rpmAWp0CiezOGydrxgG0KyPGXLLX6Zw4OPwQnDLhzKCnZzRq+aJGSB/zyTO", + "/AK3iXWw5GOZYE0KjWk/2RuC1iYjcI2G4Bt5AyLiQJEFSeQNCl34w7QYaBZmyopp/lk3Cp8AE1jjiqUw", + "Xgbgt8anvJHJ/m9ZeqSlnwcynBEDm3ilRElxoRC7Opv6x/LCP5YXRoaZGhOUYoJiAdP6395fj+X1koS8", + "ZOW0m0BzG6BsABOfzlygL3De/cF5sYoTtx80H7/Zv/vDr9hJYmonWeQToP8lX4f3UOejJLjYSUGGlznH", + "p8pp9k1f9t316ia6trHLZKRYav1DnqYMbheHU48481egXR+RCKP9y+N+7hrPxtqiFR51sI+WKU/n0Z3m", + "+ve8E2JQVgwLnxx2KfGjmrlJ+aMLjXkPcljj3GMs66YaV+XO1SuNuyuNL+fZTB1/VOBUV1pzNeZoXS9x", + "X0qJDIUHcQAgjdvw+DqFV8lp/ml1gLEAWCbZfcV8BL1wln7ELxnPmsBAJ+sm9+gWXdqeKDBemlVMYEB7", + "M6j4AasLfogM7frEuPcweq7aXH+1f/mGM78M7brzaNtZmof2lnN5zbn2xf7KY/Lw7Y8j6dH0h4wzE/Lw", + "ijSJ6J7BnyrAi64towJo+gcbOCeFhvDJtywe8MGt8+Y6tOsUTi5B+4n7XyZmDnfjJltHGUCpyBwZcd5X", + "uww4jP4v9j7lVNU4wAULOtFVaKNMVSLzRIT75FduowJVFKA6AIXcMYlgEgYWKVECpIzfKMZJCrTCwcnk", + "VXY46OfykXrk0ZYHMbm8N1d8+xeR8z6le36opeRdWPF5LSGjJ1VN0U5Xho6fmoC1a+gW0LfoKuhL/FLc", + "Ui20s9+fPXMSmEPE8FQ6dQEYODOdGj2WPZZ1d6aXgaaU1dRY6sNj2WMj6Ba3NYMQnkHaAjCuqMwApWjN", + "DOXdOCSF5jCC6wup3wBcmwfrHzTTaDbrb8sLJZVyuajm0UeZ/zKxocWRBKEyAuaLYeEPv8XGQJk2UYoB", + "wZM65/5bJlqdggl/OATadU+7V285i/ecN8us/YSVNLq0M5LoyaDZdQ80cs/BU8xz2F9J3CaqaxTb3Snd", + "pLeH7OfHemG2rZ2JYsMA1otREUHKvtcYxfpnCNp3YfU6tB/6wED7euvnO9D+Bpm2RVhd4OA3nfpsOK8X", + "wDTQhj3sDE/qhVnsPY6liN1F2C9TCAPuRDZc8yKKNBlZpLEVbo9F/TC/n5wqA30SF7ORl/nc1ZcXuTj0", + "70U99S7HUlTuDW7/r6vCXU1pKCVgAcPdCg3YLB6jun92VapvMINfomKQjqM+NCvnek3LrmNRjsZTatEC", + "BijkDkdSTnrLhxIjpCh+R9P84vvG5QVYvdVaXYP2CrqNvQ6rz31a/6kCjNmQ2KqZK+BX2iE5/My4Uiym", + "0oj4yBdkPdBhFkjAdZn+ji5WhgWlJsahXcfhCQcWD2H4fTIOFkOgAn894cYdddMrDuAzlHndQv+91nj8", + "oLn9dfMOuhB576rzbJkP3nkwi3JpaYEncO4d02pJtsPfc5JkJKnBvgiIjNrDnwxNzg65AB/rTAPy5eC9", + "LL6XxX7J4udq4WJ4MTYue+i5nW84l6H9JOJ3B7mhqJzh+DgUNXyhWCRZ+OVtXJjUwhFzJhjb956gcuMX", + "pk5zhUjkkNRlVNjA4jUpIqwkBYQ42xcPCCuHh73eRp49jzT9hy8MXu1lqBmp45PoQ0dGt+UQzE6Qy/SJ", + "2zngCgUg8jotMT0SWyExURJHRO8YN7KXfjAxL10SxZIUP0fpIMvb1I55fH5gs0zTW8ZARyg+eCaFjRKB", + "0Y5JWLsGPEpLGVv+SyNBO+qtIq/dEq3+YeJ5gNQo2xc4ElrUDCoSi/0ENK49D8Erdtw3maFAFIuNd9Ul", + "2R8IZk32BIgN95B5Ta9VR7+sf5xtPZTIMWxwp0iSVc2gEwnNpB0YeZ+IUuYdk3EQrUoUDSKTHopH+8Y8", + "WEVSQww0puVUUEVGAyVb60PB50CoOp6F7peqw/dm+HYY/y5nfz/25uo1zmiYmNyOBwkMbTgL38CGOxKy", + "tXcHiZXCDq70yh4ynOt3aO3jQchqATYJnmnfLAYoF5pDD+kDpJ2pfTPsH8Gt0nYvnDVJIgcSlWKRr4gk", + "nm/Q+o6wdPu65BB1B2WtDqI6ZnO48mgGFxDNfB7+U87TJxzi+cNQ3TFGhwY4Z7derDoLK9C+HpQAhXM2", + "Lgzq1yraxLXM8FVEBv39wqMIuuCvyQwR2QabN+gh/XF4qFdWROVi6jD7ud18dgvamx7i7IfQXiAvVcvV", + "N6b6C0VwLzPdRcad1E69MKkH+l6dbhmuDzkxgfP9gTHuxzeLmX5JpNpq9VZr9Tq0rzDZFTkoQUUCEYeU", + "KkVLLSuGlZnSjdJwwXt3xmMSAnBZ7PmFu9HDI78w3KSqKUiBJbwHUov4CneIrsNiAhb5sX/YXfJn8uaF", + "TEH/s1bUlQLX+SZZIfB1T5z+FNbmYPUJum2N71ZcC3Sfs3QT2jdRfV6vTjXbL/DZ5oR5YdwHo427hJRl", + "8n5qx84Fj+9zBi4ScEwtDKE2aXWnft25vOZu+Oo2rN2F1W9gbRP3IWAtTs9E32NJhoUgzLGyohZyk7MH", + "A8X7ONXWLSLG6rmKCYzcQRFCTZLq0GOzwGeWy69RcUoWcobvRphwJhvLy0/SXc1ozepIGdNwGip+hHP2", + "3o87rgU8kChJXuB8L0a/TDESW6f2ign5vkrs9p1Y5kT3o4XSZoZl+OUNlagmv+vYuKZzRSxSfvn/jkWq", + "k4jsvYgNhohJSZbPcaww4C+rezvfQ3vT2x6r7Qe6hPtXWNuQl60DZNEIkcYZJTfK9Tq5XoL2w+adHae2", + "CKvV4NdY75IFWL3m/eoS7obrvPLScb40DlQaKYZDZlaOoFtbmTlydqRr3+7Ok6zS/GFt//6V5p013CNU", + "oALfHaRLyRfVaySRNAmGipPpo2LovZ/rjWffBIkfV1qwhNjXoV2F1YXgGX0gOaIU4SGR5mgE93xEN+7/", + "iJzlu7C66nWXn7NR68JLGL9ESfYN9Ix/HtpXgowcWVFeMnGQkDHo9iPPA2cQupFASjrcI8YiE4Mrheci", + "XTHYCadox4c6Lu3OzDadiFQf79WFE3YThD7Tk2q8wyCq12nAJ2oUi0KyUqRJpi0PIwwyZ/zCK9JvVoOe", + "mpdwfxequabwxJdiCL+7yDv3NFUy+iIblErEXm1jWMgkbHK7uPJJnUElSnLnwayZ+Tz488UMahfDT5UQ", + "vuXDeIsXZgOJdpjC5QX0H7/njOn/4SSGq498EotoAiyxVyB/5i/TSYtZRrjpYoWol8SKI6NDencgeIA2", + "StFOUhJS0hH7HUBiPhedMlK2kO8FUkye7ArGWqQc3WtQ74gBpryqwzLAuEBmzq8AxVSxzcU3zoM11DDt", + "OXKNX+JQHhezItsTBa1DxSoWL3oWF2Oi+E71iuMU0I/eDo7n88A0h8+gApC9VBkimiIcsVypTtBDEhYt", + "gOU+LEjHt3zhGMZlMgbax4k5e52hYMHG3HM4UHDZLTob/8JbdIcdXno7xIsq1JktuX2hZoggk2Kj9jN1", + "EawL77wReB+gLBtj/4wsG8XA0lm26OwyQjuwqE3WCpUkpcB3dQ4Fge/0vbgO1A3Zg4tts7wRlLkagtUt", + "WF2DtQ1oP3UDI0IVrbhG1L6NotCnVFAaFNL0bYuzuNWqvWYKUgDboZ5wE9frOj1e66WvQ7dbY8lwWMRU", + "aOD9Vj4C807MJDDuBAF7ETcEgPY5VGC2oUv2CQKciUU0xH5EQJEzK3n/JDx2q+N+ODK+pr/QWbROUnSJ", + "pLp7Qkf63Fx5C09tj1qqjuoYKJGFIDfcuDbnfPfXgFjy4hllEJSak02/CnSBiDlQcq2n+viI0ksefVK0", + "at+1DxcUO/b+EoPke9J7Zzn1pNGSd+mJmZOZfzCOxqNWk4/pJMegIvYLBP79IeHzF+eA0FFCuw5IUpaS", + "2H46FfS8ywUamCmGkWFyabCTkR58fQ0G/C3yg4DoiEOSaVb7VQZDcHDPNF2RsYJQIDYnPx6g6dgLkYz0", + "7O2zWHI71SbGBhQShfJJEyYue2EVac9j5h/KU7nu2LW9+t6r+WZ9uV0h9XgQdVtPVPKhW8/Q9Afy+dPv", + "nkfKaYEq4ZlKuqISbHOwCLN5bd25bbfLIVJRZnd1OY6uJBX5UYswGa2pDxplegRDgt0507QdydAGQxjO", + "RHhmgEIaNhIYcU3cDEsHN7E15CRzMKKcuB+QQAApD6gi4QDxA5/DRPMv1tOigqAOPK2kcIhCiKsq6Zc8", + "/KiIGimXljtJ970+yom5Nvpwi0IYLqLYMkwNFwUyjJkFsUwc9T0RMqqde7/lTNAfPbnSTxyjYnGL04op", + "RHJv2RnkxDncwPE5cfpTWL3FeBQsIWldfcbeVZPZtdfTUggMdBO+/4VgOoGBGR5XTbKjKNF9IuhEp1iW", + "kp8pAc36X0NTahG4ePk3euFj7m4YAB+Id9p3VxloEHqsFKcMktPKQwXLb2UpXnnXlbGStBUcEAeWpfmT", + "6SFrAityKlPgzB4u1n/hppb2aju1tIm+Lf0FUq+4G30OhO3oc6WgH72cmXYWv2zc2WrWV5tLV7pacmaC", + "3ym/XzVo+mishZTgVj/pqtkWgiBjw4UTeAZdOIYfXiWhRyLWEvDTuxt3qfxNCWKwTrOKnRM6g5rwip/W", + "JNEc1mxYfQprNVjdYhk4EcFPuMtLWDwRCLkjYA/pWn1oX6y6eiSygup60F5HD6oXUsyyepHW2cHUvSi1", + "lui5ssCvJzCI0ILKs7ApZfqg/dRZ/NK5/C206/uPLjfv1wPRebs737i70qq+aFzfas3P7+1+1XqxA+fs", + "/fs3/U82oL3OKeww4QHRjzMKvOETigWmdWNW5nwCf5GQw8GDBJmbcBZ+vobEQ/d9Rw/EPruM5KriTIyP", + "IQmejjJv+/FyQAxhlOyRY4CCY2rfjJCY4GPpQDicNcExGZCol8/SNCZ4GqIiUhD8cPZwsDj4mogKVNtX", + "RCVFLeaUijWTUc2cqU5rqia4zRK+6PS65Nr15tevWhs3WJLzO0UtHq9YMxPmaTzvEXvaKuHW8PdLIjTA", + "YQynIULZppVcgWlXfRzyMBidDrgrcyIh/7cO+iOXFdP0Gviyq78FPx+h/ggkihlWg6BdUpKmRNBiwruV", + "GSW17mtHtjH3QfkaJQrmObabJLk74bsqNd4uxThnILFSFslLGBoLChuRKKyUU90K4HDpezew5PQiwDFn", + "SdXUUqXE6a+N5XCMJ7olVfsPoE1bM+TnPvF8anMeUyZ8G8jnmECwE+YwdNxYPDaF/4N4+1TY6m+aWN/b", + "ColPf+p+1ASLElxBQoVlKs6LLodvweoO6l4wj/hyC+vxGNoqJjAmxsX8jKqIJCCMBCiYlY2XJMHpni48", + "W8a60KdizgAmsDLeZ3xhjo7PWe6umiuvWqvXYW3De/+KUMuU8VPe15+4H/slPLpgHw9Rc7IQgo7QV1FC", + "edPluNo1WPubixZ7c3/O3vt51au2R9IxOo+nZCnqBLk9MWmcx9ca97eTCfDOO/ZlcjfjiqX0JXHmrwqr", + "tzCmJaiZJJqsnbDpn7mgFLEu51nbUKu5nu/T587iDzK88Cma97BKC1ieemRJuXX4/hFTyu3rqKLpMrQv", + "4QBDVqwRpKJycN4AuadAp/3ZehjMegCxUONvJuGAw5+Bn7IkNs3PWUY2232NQuzzYp/r4wTbF+YBAjQm", + "aZRwJwTDZcrAUPWCdF1KOvXmwyg6FPQpdAovNZBlKAPkJqfzQ672anvEXbhQNCK0ajvNTCwlyjP7BBqg", + "RDO9cyGS20g108QT8ftgJJuTFT073RxR7xWhducnnA8Jle+2GaGyyf0wIzNqOec1slVBYnmNORtW8e2a", + "reDWEtU8EM7ZjYXLzuvbrbnLcM52btxpvbwPa8uwto7c2A10rP60+fe/wuoXrTe70P7Z/QQJ6/6jK86r", + "RWg/hdXXaP55rA0pZwTOVf+fhqv+4pK/b3fnLd1SisdLekWz3u5eg9Vq6+c7zo1tNFQk6jNq+Xi4famy", + "MI0HD/fv3Z4YH/rAVzEb0F5urFShvdV4MNe49xyP+5XgTkzOs97ttyxpPH7Q3P6aIsPb3fm9Xbvx7TNn", + "6QZJAPevxPE5C5rzYNZL/bSRiW5s7zgLdynKD30Q9q9WrIrrRTpzjwPDxkMH0UbZ/SolWwo4uhgLSsx7", + "zuKWs/QkButJoJjqpFpsF9yp8Lt2IY4tyUStT7uwqi23TlA7fEMJ4sT429351uOrjbvP8X6dxS0+k3ja", + "Ane6zqmF6J4DPyuebaVcqnhhJF/Qna03re9WYXUTgfdo6IO9NwtjQ5VyQbFAIadY6SF86I//7MOjFnhk", + "MnXDigBZAFNKpWi5+AsmZXVE4EKIldPQB4qZH4L2G9TQc2vIHcyDQTcKVEHpEAj3u1Q6BbRKCSUW0d/Q", + "P55L97WILFMRfuKtxjJSf/gtsiBBDyuPs7BKwJo68LAoG0Vam1lB+Oi3tbhETg1rV33xveXFW3NVYWBJ", + "6/ZeuAcnEFfGV5w9pDrPDEhYRMRgFwSUdL27e89bTxaDCD6BmHyvIgM+8/ujsCvmI5PG7AdFEv3E6U+d", + "1187u8hBYNyfrkrb+X/H8LBP9jq01h3Y2F5bx/6Ys05MVi9tTx+vq//ht0MfnDj96dC4Yim/omQMPctb", + "Q8crP8HaX7Bt6Ui6kpIL2L67MXGdp0+b19abS1f89mbVhGxDRJYkSvUTi3a1VH8vQzG84wQFGeRmEu2c", + "oFOIkDBBwCOt21j0OOKJCkmbJXQ8Wusvm9vP23A8KgcjCA7R3+7OOw9fNR78zaueX9uhIlw3BmV5JxXr", + "qAlR912is8i/fudcIgx2kksUkP/yGr7IjoOmzrR3xrO8vLce8RSLF9TO2S5vVr/H16v25140biy7v1ZX", + "WnM1aNehHfJse/x42vcFBp4r8U7fed6kWITIzbbBm66LlXiUiUa1d56J5+3jcc5pd8U2znSoPYmOPdHQ", + "5LPPYMbkA9AQOz1LX3v4OLSjUA8bMolsD7+S2Wx/YzQTH/jIzadba/Ne4BIn+MNowcE7gmNhgn8aF4pF", + "u0dy/jqSamRwES2lcyrJKif5VO4wEDkAuo19PtcP3WYBJT8jKl/pDZB77H3Gn62Hno0HEAuNfFhJlPkT", + "8C0tMQ/fyBJ7FbuySzecazfELXE6eLvhvP6pZf/Ae7nhFyTobIlnT5pPb3KPjcLmOAc4dPzmIWdWv4tr", + "e+eH+7U157uF5t0NfKGRNbGh66X2JoW1eygEr6EbkxvetXC73nzw0LFfc5ZRzdxkUcmfZy1FvPaO4RpF", + "WDzQQUkxzh/uLUd8WhaoqkBUhFoqFDlS52RIF4rtTIWSiI8UhW6TL5H4b0K/958y/9RG2XhgWoY+600/", + "MW72xSbgDUO77uOgekstQPupl0y1n0L7BhfPiSXoGVsiKTNV0QqfgGnVtIABColXAVEzT/z8ICgTEJJO", + "LUgZjZORNd+1O4GJ9GwDSRKi03YUEq4kJUMD5AzTO2eEGqRLIB1lEPMm8vZgXPyTcr2SPK6K0OHihxjy", + "qDxDCFWPniLgdteoR3xPvbtwnfdu3iG5eZuoVMw3YZtjVLen6y5f42+XjrDXRwWoB/H6KuWirhRyU2oR", + "iF4m3YXVVXTqgF/F1dD5F6/CrBuHnUXznnSnFXl8pUrRUl0OzkzpRmm4oFiK6IWoD2byIX0fHq9KvAIV", + "oa0xv+R88TDagJsghkceYRML91e5FIBUgwp3uiG1gKdj3hc81niwgf+O7786l9f2dr4PlmWKH3WDI6i7", + "98eR9Oi5tPyNjnQKp1PGEPVTHUtXCIj/1HxiHL2i9m4eHnf5azQ7+s/D2ZHh7MiZbHYM/f8/3Y0W0EjV", + "9O8NjE0pRRMEm258Mec8fuo83tivfTnivan2ZveuI/Jnd1VRMjyjMXhGJeEZlYCHnP0iSaTk/vTJZzAc", + "rmV0oudlgvwZ+GkgKX5Pborcgd32ntQXuBaWeHHfgcsTFgJgm1v/16Pk+lPJCkxNoc3y+CHQiBI5Co9H", + "sA7jcAqWFsQrEtmJDjrc4bgeNZDqd54C4UEiSYFRLJmhCHcSUKTt0NcjkDDuRaQZoKA3smdGxBvoPelw", + "159RbPkHEIUio1Hh2wx+JCuJKG+eQ+zU/94o9SGWOohRChoUZqYNvVLGPayJLK2QL1HlYa9ldvWW80Pd", + "mb/iw4Suxs9V0awTBXMI2teb66/2L99w5pddWXi07SzNY3fcV/HeJzFGx1eLXF7/DYKxQ3bvTkND/Boo", + "2oW9K5nj7pvyCoW/Q7pIFgeD/4IHc5Hz5rrnDeQwd0K77lXZq95CbL5C3TeLF8YOOrqT7Jlq14fgozCd", + "Smj77v4qdxXNb+3e+yto5IsRmfAnugdm+IO4nR/++DPww58uNbY/SvqZumiGcSDUzx4WA67yS2gkvHqW", + "ZS6vSsaR5TF/vASvBVOLeY6cMYH3SOR0XwlH0dH/W45RZCQyYYjeJNVIbYzm3PaDLIpswmCLJNwABQxM", + "FDBir7ggVJLlgB9OHCI2B0DgKG+87wJ3IEGTErABk6wkiWovm+HPKLa8g3FsT8sUD7si97DC9w4TNNPE", + "4RUMfLf8ToYmkvQ73WAKGBd87FaMYmosNWNZ5bFMpqjnleKMblpjIx+OfphBhyyfDeuGOq1qSnHY/LMy", + "PQ2MYfdjDMbosWzq4v8PAAD///5tR8AwKQEA", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/api/go.mod b/api/go.mod index fb2734779..45d94dc65 100644 --- a/api/go.mod +++ b/api/go.mod @@ -7,9 +7,8 @@ toolchain go1.24.1 require ( github.com/doug-martin/goqu/v9 v9.19.0 github.com/go-sql-driver/mysql v1.8.1 - github.com/go-test/deep v1.0.8 // indirect github.com/joho/godotenv v1.5.1 - github.com/labstack/echo/v4 v4.11.4 + github.com/labstack/echo/v4 v4.12.0 github.com/minio/minio-go/v7 v7.0.83 github.com/oapi-codegen/runtime v1.1.1 github.com/pkg/errors v0.9.1 @@ -46,8 +45,10 @@ require ( ) require ( + github.com/getkin/kin-openapi v0.124.0 github.com/go-testfixtures/testfixtures/v3 v3.14.0 github.com/google/wire v0.6.0 + github.com/oapi-codegen/echo-middleware v1.0.2 ) require ( @@ -72,14 +73,21 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/swag v0.22.8 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/googleapis/go-sql-spanner v1.7.4 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/invopop/yaml v0.2.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.29.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect diff --git a/api/go.sum b/api/go.sum index 46680bc05..1572ad8b1 100644 --- a/api/go.sum +++ b/api/go.sum @@ -710,6 +710,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M= +github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= @@ -732,6 +734,10 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= +github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -835,7 +841,6 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLe github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= -github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -867,6 +872,8 @@ github.com/googleapis/go-sql-spanner v1.7.4 h1:pwndJlqgIMOewkORveYQQocaSyOGqaQg8 github.com/googleapis/go-sql-spanner v1.7.4/go.mod h1:DfuJMbqpcDQwtbol+TnfO+AUyeoW5H+w8Gm216dTPys= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -878,6 +885,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 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/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= +github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= 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 v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= @@ -900,6 +909,8 @@ github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= @@ -925,8 +936,8 @@ 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/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= -github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= +github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= +github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= @@ -935,6 +946,8 @@ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -951,10 +964,16 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.83 h1:W4Kokksvlz3OKf3OqIlzDNKd4MERlC2oN8YptwJ0+GA= github.com/minio/minio-go/v7 v7.0.83/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/oapi-codegen/echo-middleware v1.0.2 h1:oNBqiE7jd/9bfGNk/bpbX2nqWrtPc+LL4Boya8Wl81U= +github.com/oapi-codegen/echo-middleware v1.0.2/go.mod h1:5J6MFcGqrpWLXpbKGZtRPZViLIHyyyUHlkqg6dT2R4E= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= @@ -1013,6 +1032,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= @@ -1127,8 +1148,6 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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= @@ -1426,8 +1445,6 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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= @@ -1719,6 +1736,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.3.3 h1:jXG9ANrwBc4+bMvBcSl8zCfPBaVoPyBEBshA8dA93X8= diff --git a/api/internals/di/wire_gen.go b/api/internals/di/wire_gen.go index e37c25d18..32defa469 100644 --- a/api/internals/di/wire_gen.go +++ b/api/internals/di/wire_gen.go @@ -58,10 +58,10 @@ func InitializeServer() (*ServerComponents, error) { incomeExpenditureManagementUseCase := usecase.NewIncomeExpenditureManagementUseCase(incomeExpenditureManagementRepository) mailAuthRepository := repository.NewMailAuthRepository(client, crud) sessionRepository := repository.NewSessionRepository(client) - mailAuthUseCase := usecase.NewMailAuthUseCase(mailAuthRepository, sessionRepository) + userRepository := repository.NewUserRepository(client, crud) + mailAuthUseCase := usecase.NewMailAuthUseCase(mailAuthRepository, sessionRepository, userRepository, transactionRepository) objectUploadUseCase := usecase.NewObjectUploadUseCase(objectHandleRepository) passwordResetTokenRepository := repository.NewPasswordResetTokenRepository(client, crud) - userRepository := repository.NewUserRepository(client, crud) passwordResetTokenUseCase := usecase.NewPasswordResetTokenUseCase(passwordResetTokenRepository, userRepository, mailAuthRepository) sponsorRepository := repository.NewSponsorRepository(client, crud) sponsorUseCase := usecase.NewSponsorUseCase(sponsorRepository) @@ -71,7 +71,7 @@ func InitializeServer() (*ServerComponents, error) { teacherUseCase := usecase.NewTeacherUseCase(teacherRepository) userGroupRepository := repository.NewUserGroupRepository(client, crud) yearRepository := repository.NewYearRepository(client, crud) - userUseCase := usecase.NewUserUseCase(userRepository, sessionRepository, userGroupRepository, divisionRepository, yearRepository, transactionRepository) + userUseCase := usecase.NewUserUseCase(userRepository, mailAuthRepository, sessionRepository, userGroupRepository, divisionRepository, yearRepository, transactionRepository) yearUseCase := usecase.NewYearUseCase(yearRepository) sponsorshipActivityRepository := repository.NewSponsorshipActivityRepository(client, crud) sponsorshipActivityUseCase := usecase.NewSponsorshipActivityUseCase(sponsorshipActivityRepository, transactionRepository) diff --git a/api/internals/domain/mail_auth.go b/api/internals/domain/mail_auth.go index fb430915e..08c5223fd 100644 --- a/api/internals/domain/mail_auth.go +++ b/api/internals/domain/mail_auth.go @@ -15,6 +15,7 @@ type MailAuth struct { type Token struct { AccessToken string `json:"accessToken"` + UserID int `json:"userID,omitempty"` } type IsSignIn struct { diff --git a/api/internals/usecase/mail_auth_usecase.go b/api/internals/usecase/mail_auth_usecase.go index ca32c61fd..201f3b815 100644 --- a/api/internals/usecase/mail_auth_usecase.go +++ b/api/internals/usecase/mail_auth_usecase.go @@ -12,41 +12,80 @@ import ( ) type mailAuthUseCase struct { - mailAuthRep rep.MailAuthRepository - sessionRep rep.SessionRepository + mailAuthRep rep.MailAuthRepository + sessionRep rep.SessionRepository + userRep rep.UserRepository + transactionRep rep.TransactionRepository } type MailAuthUseCase interface { - SignUp(context.Context, string, string, string) (domain.Token, error) + SignUp(context.Context, string, string, string, string, string) (domain.Token, error) SignIn(context.Context, string, string) (domain.Token, error) SignOut(context.Context, string) error IsSignIn(context.Context, string) (domain.IsSignIn, error) } -func NewMailAuthUseCase(mailAuthRep rep.MailAuthRepository, sessionRep rep.SessionRepository) MailAuthUseCase { - return &mailAuthUseCase{mailAuthRep: mailAuthRep, sessionRep: sessionRep} +func NewMailAuthUseCase( + mailAuthRep rep.MailAuthRepository, + sessionRep rep.SessionRepository, + userRep rep.UserRepository, + transactionRep rep.TransactionRepository, +) MailAuthUseCase { + return &mailAuthUseCase{ + mailAuthRep: mailAuthRep, + sessionRep: sessionRep, + userRep: userRep, + transactionRep: transactionRep, + } } -func (u *mailAuthUseCase) SignUp(c context.Context, email string, password string, userID string) (domain.Token, error) { +func (u *mailAuthUseCase) SignUp(c context.Context, email string, password string, name string, bureauID string, roleID string) (domain.Token, error) { var token domain.Token + // パスワードをハッシュ化 - hashed, _ := bcrypt.GenerateFromPassword([]byte(password), 10) - mailAuthID, err := u.mailAuthRep.CreateMailAuth(c, email, string(hashed), userID) + hashed, err := bcrypt.GenerateFromPassword([]byte(password), 10) if err != nil { return token, err } + tx, err := u.transactionRep.StartTransaction(c) + if err != nil { + return token, err + } + + userID, err := u.userRep.CreateWithTx(c, tx, name, bureauID, roleID) + if err != nil { + _ = u.transactionRep.RollBack(c, tx) + return token, err + } + userIDStr := strconv.FormatInt(userID, 10) + + mailAuthID, err := u.mailAuthRep.CreateMailAuthWithTx(c, tx, email, string(hashed), userIDStr) + if err != nil { + _ = u.transactionRep.RollBack(c, tx) + return token, err + } + // トークン発行 accessToken, err := _makeRandomStr(10) if err != nil { + _ = u.transactionRep.RollBack(c, tx) return token, err } // ログイン(セッション開始) - err = u.sessionRep.Create(c, strconv.FormatInt(mailAuthID, 10), userID, accessToken) + err = u.sessionRep.CreateWithTx(c, tx, strconv.FormatInt(mailAuthID, 10), userIDStr, accessToken) if err != nil { + _ = u.transactionRep.RollBack(c, tx) + return token, err + } + + if err = u.transactionRep.Commit(c, tx); err != nil { + _ = u.transactionRep.RollBack(c, tx) return token, err } + token.AccessToken = accessToken + token.UserID = int(userID) return token, nil } @@ -54,8 +93,12 @@ func (u *mailAuthUseCase) SignIn(c context.Context, email string, password strin var mailAuth = domain.MailAuth{} var token domain.Token // メールアドレスの存在確認 - row := u.mailAuthRep.FindMailAuthByEmail(c, email) - err := row.Scan( + row, err := u.mailAuthRep.FindMailAuthByEmail(c, email) + if err != nil { + return token, err + } + + err = row.Scan( &mailAuth.ID, &mailAuth.Email, &mailAuth.Password, @@ -100,7 +143,11 @@ func (u *mailAuthUseCase) SignOut(c context.Context, accessToken string) error { func (u *mailAuthUseCase) IsSignIn(c context.Context, accessToken string) (domain.IsSignIn, error) { var session = domain.Session{} var isSignIn domain.IsSignIn - row := u.sessionRep.FindSessionByAccessToken(c, accessToken) + row, err := u.sessionRep.FindSessionByAccessToken(c, accessToken) + if err != nil { + return isSignIn, err + } + _ = row.Scan( &session.ID, &session.AuthID, diff --git a/api/internals/usecase/user_usecase.go b/api/internals/usecase/user_usecase.go index 7c8603c64..e145252b7 100644 --- a/api/internals/usecase/user_usecase.go +++ b/api/internals/usecase/user_usecase.go @@ -13,12 +13,13 @@ import ( ) type userUseCase struct { - userRep rep.UserRepository - sessionRep rep.SessionRepository - userGroupRep rep.UserGroupRepository - divisionRep rep.DivisionRepository - yearRep rep.YearRepository - transactionRepo rep.TransactionRepository + userRep rep.UserRepository + mailAuthRep rep.MailAuthRepository + sessionRep rep.SessionRepository + userGroupRep rep.UserGroupRepository + divisionRep rep.DivisionRepository + yearRep rep.YearRepository + transactionRep rep.TransactionRepository } type UserUseCase interface { @@ -32,8 +33,8 @@ type UserUseCase interface { UpdateUserGroups(context.Context, int, int, []int) (*generated.UpdateUserGroupsResponse, error) } -func NewUserUseCase(userRep rep.UserRepository, sessionRep rep.SessionRepository, userGroupRep rep.UserGroupRepository, divisionRep rep.DivisionRepository, yearRep rep.YearRepository, transactionRepo rep.TransactionRepository) UserUseCase { - return &userUseCase{userRep: userRep, sessionRep: sessionRep, userGroupRep: userGroupRep, divisionRep: divisionRep, yearRep: yearRep, transactionRepo: transactionRepo} +func NewUserUseCase(userRep rep.UserRepository, mailAuthRep rep.MailAuthRepository, sessionRep rep.SessionRepository, userGroupRep rep.UserGroupRepository, divisionRep rep.DivisionRepository, yearRep rep.YearRepository, transactionRep rep.TransactionRepository) UserUseCase { + return &userUseCase{userRep: userRep, mailAuthRep: mailAuthRep, sessionRep: sessionRep, userGroupRep: userGroupRep, divisionRep: divisionRep, yearRep: yearRep, transactionRep: transactionRep} } func (u *userUseCase) GetUsers(c context.Context, ids *[]int) ([]domain.User, error) { @@ -108,12 +109,12 @@ func (u *userUseCase) GetUserByID(c context.Context, id string) (domain.User, er func (u *userUseCase) CreateUser(c context.Context, name string, bureauID string, roleID string) (domain.User, error) { latastUser := domain.User{} - err := u.userRep.Create(c, name, bureauID, roleID) + userID, err := u.userRep.Create(c, name, bureauID, roleID) if err != nil { return latastUser, err } - row, err := u.userRep.FindNewRecord(c) + row, err := u.userRep.Find(c, strconv.FormatInt(userID, 10)) if err != nil { return latastUser, err } @@ -160,13 +161,59 @@ func (u *userUseCase) UpdateUser(c context.Context, id string, name string, bure } func (u *userUseCase) DestroyUser(c context.Context, id string) error { - err := u.userRep.Destroy(c, id) - return err + tx, err := u.transactionRep.StartTransaction(c) + if err != nil { + return err + } + + if err = u.userRep.DestroyWithTx(c, tx, id); err != nil { + _ = u.transactionRep.RollBack(c, tx) + return err + } + + if err = u.mailAuthRep.InvalidateEmailByUserIDWithTx(c, tx, id); err != nil { + _ = u.transactionRep.RollBack(c, tx) + return err + } + + if err = u.sessionRep.DestroyByUserIDWithTx(c, tx, id); err != nil { + _ = u.transactionRep.RollBack(c, tx) + return err + } + + if err = u.transactionRep.Commit(c, tx); err != nil { + _ = u.transactionRep.RollBack(c, tx) + return err + } + return nil } func (u *userUseCase) DestroyMultiUsers(c context.Context, ids []int) error { - err := u.userRep.MultiDestroy(c, ids) - return err + tx, err := u.transactionRep.StartTransaction(c) + if err != nil { + return err + } + + if err = u.userRep.MultiDestroyWithTx(c, tx, ids); err != nil { + _ = u.transactionRep.RollBack(c, tx) + return err + } + + if err = u.mailAuthRep.InvalidateEmailByUserIDsWithTx(c, tx, ids); err != nil { + _ = u.transactionRep.RollBack(c, tx) + return err + } + + if err = u.sessionRep.DestroyByUserIDsWithTx(c, tx, ids); err != nil { + _ = u.transactionRep.RollBack(c, tx) + return err + } + + if err = u.transactionRep.Commit(c, tx); err != nil { + _ = u.transactionRep.RollBack(c, tx) + return err + } + return nil } func (u *userUseCase) GetCurrentUser(c context.Context, accessToken string) (domain.User, error) { @@ -175,7 +222,11 @@ func (u *userUseCase) GetCurrentUser(c context.Context, accessToken string) (dom var row *sql.Row var err error // アクセストークンからmail_authを取得 - row = u.sessionRep.FindSessionByAccessToken(c, accessToken) + row, err = u.sessionRep.FindSessionByAccessToken(c, accessToken) + if err != nil { + return user, err + } + err = row.Scan( &session.ID, &session.AuthID, @@ -304,7 +355,7 @@ func (u *userUseCase) UpdateUserGroups(ctx context.Context, userID int, year int } // トランザクションを開始 - tx, err := u.transactionRepo.StartTransaction(ctx) + tx, err := u.transactionRep.StartTransaction(ctx) if err != nil { return nil, err } @@ -324,11 +375,11 @@ func (u *userUseCase) UpdateUserGroups(ctx context.Context, userID int, year int return err } } - return u.transactionRepo.Commit(ctx, tx) + return u.transactionRep.Commit(ctx, tx) }(tx) if err != nil { - if rollbackErr := u.transactionRepo.RollBack(ctx, tx); rollbackErr != nil { + if rollbackErr := u.transactionRep.RollBack(ctx, tx); rollbackErr != nil { log.Println(rollbackErr) } return nil, err diff --git a/api/test/signup_transaction_test.go b/api/test/signup_transaction_test.go new file mode 100644 index 000000000..d0b4cafe3 --- /dev/null +++ b/api/test/signup_transaction_test.go @@ -0,0 +1,221 @@ +package test + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/NUTFes/FinanSu/api/internals/di" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type signupResponse struct { + AccessToken string `json:"accessToken"` + UserID int `json:"userID"` +} + +func postSignup(t *testing.T, baseURL string, values map[string]any) *http.Response { + t.Helper() + + body, err := json.Marshal(values) + require.NoError(t, err) + + r, err := http.Post(baseURL+"/mail_auth/signup", "application/json", bytes.NewReader(body)) + require.NoError(t, err) + return r +} + +func countRows(t *testing.T, query string, args ...any) int { + t.Helper() + + var count int + err := db.QueryRow(query, args...).Scan(&count) + require.NoError(t, err) + return count +} + +// 正常系: サインアップ時に users、mail_auth、session が作成され、発行されたアクセストークンで current_user を取得できることを確認する +func TestSignupCreatesUserMailAuthSessionAndCurrentUser(t *testing.T) { + prepareTestDatabase(t) + + serverComponents, err := di.InitializeServer() + require.NoError(t, err) + + testServer := httptest.NewServer(serverComponents.Echo) + t.Cleanup(func() { + testServer.Close() + serverComponents.Client.CloseDB() + }) + + email := "signup-success@example.com" + name := "Signup Success User" + r := postSignup(t, testServer.URL, map[string]any{ + "email": email, + "password": "password123", + "name": name, + "bureau_id": 1, + "role_id": 1, + }) + defer r.Body.Close() + + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, r.StatusCode) + + var res signupResponse + require.NoError(t, json.Unmarshal(body, &res)) + require.NotEmpty(t, res.AccessToken) + require.NotZero(t, res.UserID) + + assert.Equal(t, 1, countRows(t, "SELECT COUNT(*) FROM users WHERE id = ? AND name = ? AND is_deleted IS FALSE", res.UserID, name)) + assert.Equal(t, 1, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ? AND user_id = ?", email, res.UserID)) + assert.Equal(t, 1, countRows(t, "SELECT COUNT(*) FROM session WHERE access_token = ? AND user_id = ?", res.AccessToken, res.UserID)) + + currentUserURL := testServer.URL + "/current_user" + req, err := http.NewRequest(http.MethodGet, currentUserURL, nil) + require.NoError(t, err) + req.Header.Set("access-token", res.AccessToken) + + currentUserRes, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer currentUserRes.Body.Close() + + assert.Equal(t, http.StatusOK, currentUserRes.StatusCode) +} + +// 異常系: mail_auth の作成に失敗した場合、同一トランザクション内で作成した users がロールバックされることを確認する +func TestSignupRollsBackUserWhenMailAuthCreateFails(t *testing.T) { + prepareTestDatabase(t) + + serverComponents, err := di.InitializeServer() + require.NoError(t, err) + + testServer := httptest.NewServer(serverComponents.Echo) + t.Cleanup(func() { + testServer.Close() + serverComponents.Client.CloseDB() + }) + + email := "signup-duplicate@example.com" + _, err = db.Exec("INSERT INTO mail_auth (email, password, user_id) VALUES (?, ?, ?)", email, "hashed-password", 1) + require.NoError(t, err) + + name := "Rollback Target User" + r := postSignup(t, testServer.URL, map[string]any{ + "email": email, + "password": "password123", + "name": name, + "bureau_id": 1, + "role_id": 1, + }) + defer r.Body.Close() + + assert.NotEqual(t, http.StatusOK, r.StatusCode) + assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM users WHERE name = ?", name)) + assert.Equal(t, 1, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ?", email)) +} + +// 異常系: 必須項目が不足しているリクエストでは BadRequest になり、users、mail_auth、session が作成されないことを確認する +func TestSignupReturnsBadRequestWhenRequiredBodyFieldsAreMissing(t *testing.T) { + prepareTestDatabase(t) + + serverComponents, err := di.InitializeServer() + require.NoError(t, err) + + testServer := httptest.NewServer(serverComponents.Echo) + t.Cleanup(func() { + testServer.Close() + serverComponents.Client.CloseDB() + }) + + email := "signup-missing-fields@example.com" + beforeUsers := countRows(t, "SELECT COUNT(*) FROM users") + beforeSessions := countRows(t, "SELECT COUNT(*) FROM session") + r := postSignup(t, testServer.URL, map[string]any{ + "email": email, + "password": "password123", + "role_id": 1, + }) + defer r.Body.Close() + + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, beforeUsers, countRows(t, "SELECT COUNT(*) FROM users")) + assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ?", email)) + assert.Equal(t, beforeSessions, countRows(t, "SELECT COUNT(*) FROM session")) +} + +// 異常系: OpenAPI スキーマに違反する値では BadRequest になり、関連レコードが作成されないことを確認する +func TestSignupReturnsBadRequestWhenBodyViolatesOpenAPISchema(t *testing.T) { + prepareTestDatabase(t) + + serverComponents, err := di.InitializeServer() + require.NoError(t, err) + + testServer := httptest.NewServer(serverComponents.Echo) + t.Cleanup(func() { + testServer.Close() + serverComponents.Client.CloseDB() + }) + + tests := []struct { + name string + email string + values map[string]any + }{ + { + // name が空文字の場合にバリデーションエラーになることを確認する + name: "empty name", + email: "signup-empty-name@example.com", + values: map[string]any{ + "email": "signup-empty-name@example.com", + "password": "password123", + "name": "", + "bureau_id": 1, + "role_id": 1, + }, + }, + { + // bureau_id が 0 の場合にバリデーションエラーになることを確認する + name: "zero bureau id", + email: "signup-zero-bureau@example.com", + values: map[string]any{ + "email": "signup-zero-bureau@example.com", + "password": "password123", + "name": "Zero Bureau User", + "bureau_id": 0, + "role_id": 1, + }, + }, + { + // role_id が 0 の場合にバリデーションエラーになることを確認する + name: "zero role id", + email: "signup-zero-role@example.com", + values: map[string]any{ + "email": "signup-zero-role@example.com", + "password": "password123", + "name": "Zero Role User", + "bureau_id": 1, + "role_id": 0, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + beforeUsers := countRows(t, "SELECT COUNT(*) FROM users") + beforeSessions := countRows(t, "SELECT COUNT(*) FROM session") + r := postSignup(t, testServer.URL, tt.values) + defer r.Body.Close() + + assert.Equal(t, http.StatusBadRequest, r.StatusCode) + assert.Equal(t, beforeUsers, countRows(t, "SELECT COUNT(*) FROM users")) + assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM mail_auth WHERE email = ?", tt.email)) + assert.Equal(t, beforeSessions, countRows(t, "SELECT COUNT(*) FROM session")) + }) + } +} diff --git a/api/test/user_delete_transaction_test.go b/api/test/user_delete_transaction_test.go new file mode 100644 index 000000000..6379b7831 --- /dev/null +++ b/api/test/user_delete_transaction_test.go @@ -0,0 +1,104 @@ +package test + +import ( + "bytes" + "database/sql" + "encoding/json" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/NUTFes/FinanSu/api/internals/di" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func insertMailAuthAndSession(t *testing.T, userID int, email string, accessToken string) int64 { + t.Helper() + + result, err := db.Exec("INSERT INTO mail_auth (email, password, user_id) VALUES (?, ?, ?)", email, "hashed-password", userID) + require.NoError(t, err) + + authID, err := result.LastInsertId() + require.NoError(t, err) + + _, err = db.Exec("INSERT INTO session (auth_id, user_id, access_token) VALUES (?, ?, ?)", authID, userID, accessToken) + require.NoError(t, err) + + return authID +} + +func nullableEmailByUserID(t *testing.T, userID int) sql.NullString { + t.Helper() + + var email sql.NullString + err := db.QueryRow("SELECT email FROM mail_auth WHERE user_id = ?", userID).Scan(&email) + require.NoError(t, err) + return email +} + +// 正常系: ユーザー単体削除時に users の論理削除、mail_auth のメール無効化、session 削除が同一トランザクションで実行されることを確認する +func TestDestroyUserClearsMailAuthAndSessionInTransaction(t *testing.T) { + prepareTestDatabase(t) + + serverComponents, err := di.InitializeServer() + require.NoError(t, err) + + testServer := httptest.NewServer(serverComponents.Echo) + t.Cleanup(func() { + testServer.Close() + serverComponents.Client.CloseDB() + }) + + userID := 1 + insertMailAuthAndSession(t, userID, "delete-user@example.com", "delete-user-token") + + req, err := http.NewRequest(http.MethodDelete, testServer.URL+"/users/"+strconv.Itoa(userID), nil) + require.NoError(t, err) + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, 1, countRows(t, "SELECT COUNT(*) FROM users WHERE id = ? AND is_deleted IS TRUE", userID)) + assert.False(t, nullableEmailByUserID(t, userID).Valid) + assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM session WHERE user_id = ?", userID)) +} + +// 正常系: ユーザー複数削除時に対象全員の users 論理削除、mail_auth のメール無効化、session 削除が同一トランザクションで実行されることを確認する +func TestDestroyMultiUsersClearsMailAuthAndSessionInTransaction(t *testing.T) { + prepareTestDatabase(t) + + serverComponents, err := di.InitializeServer() + require.NoError(t, err) + + testServer := httptest.NewServer(serverComponents.Echo) + t.Cleanup(func() { + testServer.Close() + serverComponents.Client.CloseDB() + }) + + userIDs := []int{1, 2} + insertMailAuthAndSession(t, userIDs[0], "delete-user-1@example.com", "delete-user-token-1") + insertMailAuthAndSession(t, userIDs[1], "delete-user-2@example.com", "delete-user-token-2") + + body, err := json.Marshal(map[string]any{"deleteIDs": userIDs}) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodDelete, testServer.URL+"/users/delete", bytes.NewReader(body)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + res, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + assert.Equal(t, http.StatusOK, res.StatusCode) + for _, userID := range userIDs { + assert.Equal(t, 1, countRows(t, "SELECT COUNT(*) FROM users WHERE id = ? AND is_deleted IS TRUE", userID)) + assert.False(t, nullableEmailByUserID(t, userID).Valid) + assert.Equal(t, 0, countRows(t, "SELECT COUNT(*) FROM session WHERE user_id = ?", userID)) + } +} diff --git a/compose.e2e.yml b/compose.e2e.yml new file mode 100644 index 000000000..7b4bc9f75 --- /dev/null +++ b/compose.e2e.yml @@ -0,0 +1,125 @@ +services: + db: + image: mysql:8.0 + environment: + MYSQL_DATABASE: finansu_db + MYSQL_USER: finansu + MYSQL_PASSWORD: password + MYSQL_ROOT_PASSWORD: root + TZ: Asia/Tokyo + volumes: + - e2e-db-data:/var/lib/mysql + - ./my.cnf:/etc/mysql/conf.d/my.cnf + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"] + interval: 5s + timeout: 5s + retries: 20 + + migrate: + image: migrate/migrate:latest + volumes: + - ./mysql/migrations:/migrations + command: + [ + "-path", + "/migrations", + "-database", + "mysql://finansu:password@tcp(db:3306)/finansu_db", + "up", + ] + depends_on: + db: + condition: service_healthy + + seed: + image: mysql:8.0 + volumes: + - ./mysql/e2e_seed.sql:/e2e_seed.sql + command: > + bash -lc ' + mysql -h db -u finansu -ppassword finansu_db < /e2e_seed.sql + ' + depends_on: + migrate: + condition: service_completed_successfully + + minio: + image: minio/minio:latest + command: "server /data --console-address :9001" + environment: + MINIO_ROOT_USER: user + MINIO_ROOT_PASSWORD: password + + api: + build: + context: ./api + dockerfile: dev.Dockerfile + user: root + volumes: + - ./api:/app + - ./openapi:/openapi + - e2e-go-cache:/go/cache + - e2e-go-mod-cache:/go/pkg/mod + environment: + NUTMEG_DB_USER: finansu + NUTMEG_DB_PASSWORD: password + NUTMEG_DB_HOST: db + NUTMEG_DB_PORT: 3306 + NUTMEG_DB_NAME: finansu_db + MINIO_ENDPOINT: minio:9000 + MINIO_ACCESS_KEY: user + MINIO_SECRET_KEY: password + MINIO_USE_SSL: "false" + RESET_PASSWORD_URL: "http://view:3000/reset_password" + command: go run main.go + depends_on: + seed: + condition: service_completed_successfully + minio: + condition: service_started + + view: + build: ./view + user: root + volumes: + - ./view:/app + - ./openapi:/openapi + - e2e-pnpm-store:/app/.pnpm-store + environment: + CI: "true" + NEXT_PUBLIC_APP_ENV: e2e + NEXT_PUBLIC_ENDPOINT: minio + NEXT_PUBLIC_PORT: 9000 + NEXT_PUBLIC_BUCKET_NAME: finansu + NEXT_PUBLIC_MINIO_ENDPONT: http://minio:9000 + NEXT_PUBLIC_ACCESS_KEY: user + NEXT_PUBLIC_SECRET_KEY: password + RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED: false + command: sh -c "pnpm install && pnpm exec next dev -H 0.0.0.0" + depends_on: + api: + condition: service_started + + e2e: + image: mcr.microsoft.com/playwright:v1.57.0-noble + working_dir: /workspace/view/next-project/e2e + volumes: + - .:/workspace + - e2e-node-modules:/workspace/view/next-project/e2e/node_modules + environment: + BASE_URL: http://view:3000 + API_URL: http://api:1323 + command: bash -lc "npm ci && npx playwright test" + depends_on: + view: + condition: service_started + api: + condition: service_started + +volumes: + e2e-db-data: + e2e-go-cache: + e2e-go-mod-cache: + e2e-pnpm-store: + e2e-node-modules: diff --git a/mysql/e2e_seed.sql b/mysql/e2e_seed.sql new file mode 100644 index 000000000..4b2a11139 --- /dev/null +++ b/mysql/e2e_seed.sql @@ -0,0 +1,13 @@ +USE finansu_db; + +INSERT INTO bureaus (id, name) +VALUES (1, '総務局'); + +INSERT INTO roles (id, name) +VALUES (1, 'user'); + +INSERT INTO years (id, year) +VALUES (1, 2025); + +INSERT INTO year_periods (year_id, started_at, ended_at) +VALUES (1, '2024-11-15 00:00:00', '2025-11-15 00:00:00'); diff --git a/openapi/config.yaml b/openapi/config.yaml index 3f5d03003..9df81ed1f 100644 --- a/openapi/config.yaml +++ b/openapi/config.yaml @@ -5,4 +5,5 @@ package: generated generate: - server - types + - embedded-spec output: /app/generated/openapi_gen.go diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index c6d3b18dd..697c50cda 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1554,25 +1554,39 @@ paths: tags: - mail_auth description: ユーザー登録 - parameters: - - name: email - in: query - description: email - required: true - schema: - type: string - - name: password - in: query - description: password - required: true - schema: - type: string - - name: user_id - in: query - description: user_id - required: true - schema: - type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - email + - password + - name + - bureau_id + - role_id + properties: + email: + type: string + minLength: 1 + description: email + password: + type: string + minLength: 1 + description: password + name: + type: string + minLength: 1 + description: name + bureau_id: + type: integer + minimum: 1 + description: bureau_id + role_id: + type: integer + minimum: 1 + description: role_id responses: "200": description: ユーザー登録完了 @@ -1580,6 +1594,16 @@ paths: application/json: schema: type: object + required: + - accessToken + - userID + properties: + accessToken: + type: string + description: アクセストークン + userID: + type: integer + description: ユーザーID x-codegen-request-body-name: mailAuthSignUp /mail_auth/signin: post: @@ -3794,4 +3818,4 @@ components: - money - goods -x-original-swagger-version: "2.0" \ No newline at end of file +x-original-swagger-version: "2.0" diff --git a/view/next-project/.gitignore b/view/next-project/.gitignore index 6317051f0..80b093488 100644 --- a/view/next-project/.gitignore +++ b/view/next-project/.gitignore @@ -7,6 +7,9 @@ # testing /coverage +/e2e/node_modules/ +/e2e/test-results/ +/e2e/playwright-report/ # next.js /.next/ @@ -35,4 +38,3 @@ yarn-error.log* # typescript *.tsbuildinfo - diff --git a/view/next-project/e2e/package-lock.json b/view/next-project/e2e/package-lock.json new file mode 100644 index 000000000..44c79ff80 --- /dev/null +++ b/view/next-project/e2e/package-lock.json @@ -0,0 +1,109 @@ +{ + "name": "finansu-e2e", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "finansu-e2e", + "devDependencies": { + "@playwright/test": "1.57.0", + "@types/node": "20.19.0", + "typescript": "5.9.3" + } + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "20.19.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz", + "integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/view/next-project/e2e/package.json b/view/next-project/e2e/package.json new file mode 100644 index 000000000..b9a4c062f --- /dev/null +++ b/view/next-project/e2e/package.json @@ -0,0 +1,13 @@ +{ + "name": "finansu-e2e", + "private": true, + "scripts": { + "test": "playwright test", + "type-check": "tsc --noEmit -p tsconfig.json" + }, + "devDependencies": { + "@playwright/test": "1.57.0", + "@types/node": "20.19.0", + "typescript": "5.9.3" + } +} diff --git a/view/next-project/e2e/playwright.config.ts b/view/next-project/e2e/playwright.config.ts new file mode 100644 index 000000000..275823b82 --- /dev/null +++ b/view/next-project/e2e/playwright.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + timeout: 60_000, + expect: { + timeout: 10_000, + }, + reporter: [['list']], + use: { + baseURL: process.env.BASE_URL || 'http://view:3000', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + retries: 1, + workers: 1, +}); diff --git a/view/next-project/e2e/tests/signup.spec.ts b/view/next-project/e2e/tests/signup.spec.ts new file mode 100644 index 000000000..732c8090d --- /dev/null +++ b/view/next-project/e2e/tests/signup.spec.ts @@ -0,0 +1,76 @@ +import { expect, test } from '@playwright/test'; + +const apiURL = process.env.API_URL || 'http://api:1323'; + +async function waitForService(url: string) { + const deadline = Date.now() + 120_000; + let lastError: unknown; + + while (Date.now() < deadline) { + try { + const res = await fetch(url); + if (res.ok) { + return; + } + lastError = new Error(`HTTP ${res.status}`); + } catch (error) { + lastError = error; + } + await new Promise((resolve) => setTimeout(resolve, 1_000)); + } + + throw new Error(`Service did not become ready: ${url}. Last error: ${String(lastError)}`); +} + +test.beforeAll(async () => { + await waitForService(`${apiURL}/`); + await waitForService(`${process.env.BASE_URL || 'http://view:3000'}/`); +}, 120_000); + +test('新規登録後に current_user が 404 にならず My Page に遷移する', async ({ page }) => { + const currentUserStatuses: number[] = []; + + page.on('response', (response) => { + const url = response.url(); + if (url.includes('/current_user')) { + currentUserStatuses.push(response.status()); + } + }); + + await page.goto('/'); + await page.getByRole('button', { name: '新規登録' }).click(); + + const timestamp = Date.now(); + const email = `e2e-signup-${timestamp}@example.com`; + const name = `E2E Signup ${timestamp}`; + + await page.getByLabel('名前').fill(name); + await page.getByLabel('メールアドレス').fill(email); + await page.getByLabel('パスワード', { exact: true }).fill('password123'); + await page.getByLabel('パスワード確認').fill('password123'); + + const signupRequestPromise = page.waitForRequest( + (request) => request.url().includes('/mail_auth/signup') && request.method() === 'POST', + ); + + await page.getByRole('button', { name: '登録' }).click(); + + const signupRequest = await signupRequestPromise; + const signupRequestURL = new URL(signupRequest.url()); + const signupRequestBody = JSON.parse(signupRequest.postData() ?? '{}'); + + expect(signupRequestURL.search).toBe(''); + expect(signupRequestBody).toMatchObject({ + email, + password: 'password123', + name, + bureau_id: 1, + role_id: 1, + }); + + await expect(page).toHaveURL(/\/my_page/); + await expect(page.getByRole('heading', { name: 'My Page' })).toBeVisible(); + + expect(currentUserStatuses).toContain(200); + expect(currentUserStatuses).not.toContain(404); +}); diff --git a/view/next-project/e2e/tsconfig.json b/view/next-project/e2e/tsconfig.json new file mode 100644 index 000000000..0a2e9c5af --- /dev/null +++ b/view/next-project/e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["playwright.config.ts", "tests/**/*.ts"] +} diff --git a/view/next-project/eslint.config.mjs b/view/next-project/eslint.config.mjs index b11da5bcd..5e1bca981 100644 --- a/view/next-project/eslint.config.mjs +++ b/view/next-project/eslint.config.mjs @@ -51,7 +51,7 @@ export default defineConfig([ jsx: true, }, - project: ['./tsconfig.json', './tsconfig.stories.json'], + project: ['./tsconfig.json', './tsconfig.stories.json', './e2e/tsconfig.json'], tsconfigRootDir: __dirname, }, }, diff --git a/view/next-project/next.config.js b/view/next-project/next.config.js index 46f0e6723..fd2ec825f 100644 --- a/view/next-project/next.config.js +++ b/view/next-project/next.config.js @@ -17,6 +17,10 @@ const apiConfig = { SSR_API_URI: 'http://nutfes-finansu-api:1323', CSR_API_URI: 'http://localhost:1323', }, + e2e: { + SSR_API_URI: 'http://api:1323', + CSR_API_URI: 'http://api:1323', + }, }; // 現在の環境に合った設定を展開(該当しない場合はdevelopment設定を利用) diff --git a/view/next-project/src/components/auth/SignUpView.tsx b/view/next-project/src/components/auth/SignUpView.tsx index b60e7ef0c..dbfceb088 100644 --- a/view/next-project/src/components/auth/SignUpView.tsx +++ b/view/next-project/src/components/auth/SignUpView.tsx @@ -3,9 +3,8 @@ import React, { useState } from 'react'; import { useForm } from 'react-hook-form'; import { BUREAUS } from '@/constants/bureaus'; +import { postMailAuthSignup } from '@/generated/hooks'; import { useAuthStore, useUserStore } from '@/store'; -import { signUp } from '@api/signUp'; -import { post } from '@api/user'; import { PrimaryButton } from '@components/common'; import LoadingButton from '@components/common/LoadingButton'; import { SignUp, User } from '@type/common'; @@ -17,9 +16,7 @@ export default function SignUpView() { // 新規登録中フラグ const [isSignUpNow, setIsSignUpNow] = useState(false); - const [postUserData, setPostUserData] = useState({ - id: 0, - name: '', + const [postUserData, setPostUserData] = useState>({ bureauID: 1, roleID: 1, }); @@ -34,34 +31,30 @@ export default function SignUpView() { }); const userDataHandler = - (input: string) => - (e: React.ChangeEvent | React.ChangeEvent) => { - setPostUserData({ ...postUserData, [input]: e.target.value }); + (input: 'bureauID' | 'roleID') => (e: React.ChangeEvent) => { + setPostUserData({ ...postUserData, [input]: Number(e.target.value) }); }; const postUser = async (data: SignUp) => { setIsSignUpNow(true); - const userUrl: string = process.env.CSR_API_URI + '/users'; - const signUpUrl: string = process.env.CSR_API_URI + '/mail_auth/signup'; - // signIn には登録したuserのIDが必要なので先にUserをpost - const newUser = await post(userUrl, postUserData); - if (!newUser || !newUser.id) { - alert('ユーザーの作成に失敗しました。'); - setIsSignUpNow(false); - return; - } - const userID: number = newUser.id; - // signUp - const req = await signUp(signUpUrl, data, userID); - const res = await req.json(); - // state用のuserのデータ - const userData: User = { - id: userID, - name: postUserData.name, - bureauID: Number(postUserData.bureauID), - roleID: postUserData.roleID, - }; - if (req.status === 200) { + + try { + const req = await postMailAuthSignup({ + email: data.email, + password: data.password, + name: data.name, + bureau_id: postUserData.bureauID, + role_id: postUserData.roleID, + }); + const res = req.data; + + // state用のuserのデータ + const userData: User = { + id: res.userID, + name: data.name, + bureauID: postUserData.bureauID, + roleID: postUserData.roleID, + }; // state用のauthのデータ const authData = { isSignIn: true, @@ -69,11 +62,9 @@ export default function SignUpView() { }; setAuth(authData); setUser(userData); - Router.push('/purchaseorders'); - } else { - alert( - '新規登録に失敗しました。メールアドレスもしくはパスワードがすでに登録されている可能性があります', - ); + Router.push('/my_page'); + } catch { + alert('新規登録に失敗しました。このメールアドレスは既に登録されている可能性があります。'); setIsSignUpNow(false); } }; @@ -83,16 +74,29 @@ export default function SignUpView() {
-

名前

+ -

所属局

+ -

メールアドレス

+ -

パスワード

+ -

パスワード確認

+
+

{errors.name && errors.name.message}

{errors.email && errors.email.message}

{errors.password && errors.password.message}

diff --git a/view/next-project/src/generated/hooks.ts b/view/next-project/src/generated/hooks.ts index e32d79348..ec0acd7fb 100644 --- a/view/next-project/src/generated/hooks.ts +++ b/view/next-project/src/generated/hooks.ts @@ -111,7 +111,7 @@ import type { PostMailAuthSignin200, PostMailAuthSigninParams, PostMailAuthSignup200, - PostMailAuthSignupParams, + PostMailAuthSignupBody, PostPasswordResetId200, PostPasswordResetIdValid200, PostPasswordResetIdValidParams, @@ -4954,65 +4954,50 @@ export type postMailAuthSignupResponseSuccess = postMailAuthSignupResponse200 & }; export type postMailAuthSignupResponse = postMailAuthSignupResponseSuccess; -export const getPostMailAuthSignupUrl = (params: PostMailAuthSignupParams) => { - const normalizedParams = new URLSearchParams(); - - Object.entries(params || {}).forEach(([key, value]) => { - if (value !== undefined) { - normalizedParams.append(key, value === null ? 'null' : value.toString()); - } - }); - - const stringifiedParams = normalizedParams.toString(); - - return stringifiedParams.length > 0 - ? `/mail_auth/signup?${stringifiedParams}` - : `/mail_auth/signup`; +export const getPostMailAuthSignupUrl = () => { + return `/mail_auth/signup`; }; export const postMailAuthSignup = async ( - params: PostMailAuthSignupParams, + postMailAuthSignupBody: PostMailAuthSignupBody, options?: RequestInit, ): Promise => { - return customFetch(getPostMailAuthSignupUrl(params), { + return customFetch(getPostMailAuthSignupUrl(), { ...options, method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify(postMailAuthSignupBody), }); }; export const getPostMailAuthSignupMutationFetcher = ( - params: PostMailAuthSignupParams, options?: SecondParameter, ) => { - return (_: Key, __: { arg: Arguments }) => { - return postMailAuthSignup(params, options); + return (_: Key, { arg }: { arg: PostMailAuthSignupBody }) => { + return postMailAuthSignup(arg, options); }; }; -export const getPostMailAuthSignupMutationKey = (params: PostMailAuthSignupParams) => - [`/mail_auth/signup`, ...(params ? [params] : [])] as const; +export const getPostMailAuthSignupMutationKey = () => [`/mail_auth/signup`] as const; export type PostMailAuthSignupMutationResult = NonNullable< Awaited> >; export type PostMailAuthSignupMutationError = unknown; -export const usePostMailAuthSignup = ( - params: PostMailAuthSignupParams, - options?: { - swr?: SWRMutationConfiguration< - Awaited>, - TError, - Key, - Arguments, - Awaited> - > & { swrKey?: string }; - request?: SecondParameter; - }, -) => { +export const usePostMailAuthSignup = (options?: { + swr?: SWRMutationConfiguration< + Awaited>, + TError, + Key, + PostMailAuthSignupBody, + Awaited> + > & { swrKey?: string }; + request?: SecondParameter; +}) => { const { swr: swrOptions, request: requestOptions } = options ?? {}; - const swrKey = swrOptions?.swrKey ?? getPostMailAuthSignupMutationKey(params); - const swrFn = getPostMailAuthSignupMutationFetcher(params, requestOptions); + const swrKey = swrOptions?.swrKey ?? getPostMailAuthSignupMutationKey(); + const swrFn = getPostMailAuthSignupMutationFetcher(requestOptions); const query = useSWRMutation(swrKey, swrFn, swrOptions); diff --git a/view/next-project/src/generated/model/index.ts b/view/next-project/src/generated/model/index.ts index 6b92e3bd8..2cebb2472 100644 --- a/view/next-project/src/generated/model/index.ts +++ b/view/next-project/src/generated/model/index.ts @@ -163,7 +163,7 @@ export * from './postIncomeExpenditureManagementsCheckIdBody'; export * from './postMailAuthSignin200'; export * from './postMailAuthSigninParams'; export * from './postMailAuthSignup200'; -export * from './postMailAuthSignupParams'; +export * from './postMailAuthSignupBody'; export * from './postPasswordResetId200'; export * from './postPasswordResetIdValid200'; export * from './postPasswordResetIdValidParams'; diff --git a/view/next-project/src/generated/model/postMailAuthSignup200.ts b/view/next-project/src/generated/model/postMailAuthSignup200.ts index b3d2d26c4..54afda63f 100644 --- a/view/next-project/src/generated/model/postMailAuthSignup200.ts +++ b/view/next-project/src/generated/model/postMailAuthSignup200.ts @@ -6,4 +6,9 @@ * OpenAPI spec version: 2.0.0 */ -export type PostMailAuthSignup200 = { [key: string]: unknown }; +export type PostMailAuthSignup200 = { + /** アクセストークン */ + accessToken: string; + /** ユーザーID */ + userID: number; +}; diff --git a/view/next-project/src/generated/model/postMailAuthSignupParams.ts b/view/next-project/src/generated/model/postMailAuthSignupBody.ts similarity index 51% rename from view/next-project/src/generated/model/postMailAuthSignupParams.ts rename to view/next-project/src/generated/model/postMailAuthSignupBody.ts index 4fe580e95..ec47daf51 100644 --- a/view/next-project/src/generated/model/postMailAuthSignupParams.ts +++ b/view/next-project/src/generated/model/postMailAuthSignupBody.ts @@ -6,17 +6,30 @@ * OpenAPI spec version: 2.0.0 */ -export type PostMailAuthSignupParams = { +export type PostMailAuthSignupBody = { /** * email + * @minLength 1 */ email: string; /** * password + * @minLength 1 */ password: string; /** - * user_id + * name + * @minLength 1 */ - user_id: number; + name: string; + /** + * bureau_id + * @minimum 1 + */ + bureau_id: number; + /** + * role_id + * @minimum 1 + */ + role_id: number; }; diff --git a/view/next-project/src/type/common.ts b/view/next-project/src/type/common.ts index b63552d63..a4b883a60 100644 --- a/view/next-project/src/type/common.ts +++ b/view/next-project/src/type/common.ts @@ -218,6 +218,7 @@ export interface User { // // SignUp export interface SignUp { + name: string; email: string; password: string; passwordConfirmation: string; diff --git a/view/next-project/src/utils/api/signUp.ts b/view/next-project/src/utils/api/signUp.ts deleted file mode 100644 index 156f8a989..000000000 --- a/view/next-project/src/utils/api/signUp.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { SignUp } from '@type/common'; - -export const signUp = async (url: string, data: SignUp, userID: number) => { - const email = data.email; - const password = data.password; - const postUrl = url + '?email=' + email + '&password=' + password + '&user_id=' + userID; - const res = await fetch(postUrl, { - method: 'POST', - mode: 'cors', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); - return await res; -}; diff --git a/view/next-project/tsconfig.json b/view/next-project/tsconfig.json index b2e6b5180..0dc36cfcb 100644 --- a/view/next-project/tsconfig.json +++ b/view/next-project/tsconfig.json @@ -19,5 +19,5 @@ }, "extends": "./tsconfig.paths.json", "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "e2e"] }