diff options
Diffstat (limited to 'backend')
| -rw-r--r-- | backend/api/generated.go | 57 | ||||
| -rw-r--r-- | backend/api/handler.go | 5 | ||||
| -rw-r--r-- | backend/auth/auth.go | 109 | ||||
| -rw-r--r-- | backend/db/models.go | 5 | ||||
| -rw-r--r-- | backend/db/query.sql.go | 42 | ||||
| -rw-r--r-- | backend/fixtures/dev.sql | 3 | ||||
| -rw-r--r-- | backend/query.sql | 15 | ||||
| -rw-r--r-- | backend/schema.sql | 6 |
8 files changed, 211 insertions, 31 deletions
diff --git a/backend/api/generated.go b/backend/api/generated.go index 3c52f84..33f1a78 100644 --- a/backend/api/generated.go +++ b/backend/api/generated.go @@ -257,8 +257,9 @@ type GetGameParams struct { // PostLoginJSONBody defines parameters for PostLogin. type PostLoginJSONBody struct { - Password string `json:"password"` - Username string `json:"username"` + Password string `json:"password"` + RegistrationToken *string `json:"registration_token,omitempty"` + Username string `json:"username"` } // GetTokenParams defines parameters for GetToken. @@ -1106,32 +1107,32 @@ func (sh *strictHandler) GetToken(ctx echo.Context, params GetTokenParams) error // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "G+PQ1yEAAA==", + "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", + "BmCWZdm/AQAA//8u6d2yGyIAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/backend/api/handler.go b/backend/api/handler.go index 57d7464..23c3cfe 100644 --- a/backend/api/handler.go +++ b/backend/api/handler.go @@ -3,6 +3,7 @@ package api import ( "context" "errors" + "log" "net/http" "github.com/jackc/pgx/v5" @@ -24,8 +25,10 @@ type GameHubsInterface interface { func (h *Handler) PostLogin(ctx context.Context, request PostLoginRequestObject) (PostLoginResponseObject, error) { username := request.Body.Username password := request.Body.Password - userID, err := auth.Login(ctx, h.q, username, password) + registrationToken := request.Body.RegistrationToken + userID, err := auth.Login(ctx, h.q, username, password, registrationToken) if err != nil { + log.Printf("login failed: %v", err) return PostLogin401JSONResponse{ UnauthorizedJSONResponse: UnauthorizedJSONResponse{ Message: "Invalid username or password", diff --git a/backend/auth/auth.go b/backend/auth/auth.go index 401773f..3a292d9 100644 --- a/backend/auth/auth.go +++ b/backend/auth/auth.go @@ -1,17 +1,42 @@ package auth import ( + "bytes" "context" + "encoding/json" + "errors" "fmt" + "net/http" + "net/url" + "github.com/jackc/pgx/v5" "golang.org/x/crypto/bcrypt" "github.com/nsfisis/iosdc-japan-2024-albatross/backend/db" ) -func Login(ctx context.Context, queries *db.Queries, username, password string) (int, error) { +var ( + ErrInvalidRegistrationToken = errors.New("invalid registration token") + ErrNoRegistrationToken = errors.New("no registration token") + ErrForteeLoginFailed = errors.New("fortee login failed") +) + +func Login( + ctx context.Context, + queries *db.Queries, + username string, + password string, + registrationToken *string, +) (int, error) { userAuth, err := queries.GetUserAuthByUsername(ctx, username) if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + err := signup(ctx, queries, username, password, registrationToken) + if err != nil { + return 0, err + } + return Login(ctx, queries, username, password, nil) + } return 0, err } if userAuth.AuthType == "password" { @@ -24,6 +49,86 @@ func Login(ctx context.Context, queries *db.Queries, username, password string) return 0, err } return int(userAuth.UserID), nil + } else if userAuth.AuthType == "fortee" { + if err := verifyForteeAccount(ctx, username, password); err != nil { + return 0, err + } + return int(userAuth.UserID), nil + } + panic(fmt.Sprintf("unexpected auth type: %s", userAuth.AuthType)) +} + +func signup( + ctx context.Context, + queries *db.Queries, + username string, + password string, + registrationToken *string, +) error { + if err := verifyRegistrationToken(ctx, queries, registrationToken); err != nil { + return err + } + if err := verifyForteeAccount(ctx, username, password); err != nil { + return err + } + + // TODO: transaction + userID, err := queries.CreateUser(ctx, username) + if err != nil { + return err + } + if err := queries.CreateUserAuth(ctx, db.CreateUserAuthParams{ + UserID: userID, + AuthType: "fortee", + }); err != nil { + return err + } + return nil +} + +func verifyRegistrationToken(ctx context.Context, queries *db.Queries, registrationToken *string) error { + if registrationToken == nil { + return ErrNoRegistrationToken + } + exists, err := queries.IsRegistrationTokenValid(ctx, *registrationToken) + if err != nil { + return err + } + if !exists { + return ErrInvalidRegistrationToken + } + return nil +} + +func verifyForteeAccount(_ context.Context, username string, password string) error { + reqData := url.Values{} + reqData.Set("username", username) + reqData.Set("password", password) + reqBody := reqData.Encode() + + req, err := http.NewRequest("POST", "https://fortee.jp/api/user/login", bytes.NewBufferString(reqBody)) + if err != nil { + return fmt.Errorf("http.NewRequest failed: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return fmt.Errorf("client.Do failed: %v", err) + } + defer res.Body.Close() + + resData := struct { + LoggedIn bool `json:"loggedIn"` + }{} + if err := json.NewDecoder(res.Body).Decode(&resData); err != nil { + return fmt.Errorf("json.Decode failed: %v", err) + } + + if !resData.LoggedIn { + return ErrForteeLoginFailed } - return 0, fmt.Errorf("not implemented") + return nil } diff --git a/backend/db/models.go b/backend/db/models.go index 5bca7b7..800c183 100644 --- a/backend/db/models.go +++ b/backend/db/models.go @@ -30,6 +30,11 @@ type Problem struct { Description string } +type RegistrationToken struct { + RegistrationTokenID int32 + Token string +} + type Submission struct { SubmissionID int32 GameID int32 diff --git a/backend/db/query.sql.go b/backend/db/query.sql.go index dc87602..47140fc 100644 --- a/backend/db/query.sql.go +++ b/backend/db/query.sql.go @@ -106,6 +106,34 @@ func (q *Queries) CreateTestcaseResult(ctx context.Context, arg CreateTestcaseRe return err } +const createUser = `-- name: CreateUser :one +INSERT INTO users (username, display_name, is_admin) +VALUES ($1, $1, false) +RETURNING user_id +` + +func (q *Queries) CreateUser(ctx context.Context, username string) (int32, error) { + row := q.db.QueryRow(ctx, createUser, username) + var user_id int32 + err := row.Scan(&user_id) + return user_id, err +} + +const createUserAuth = `-- name: CreateUserAuth :exec +INSERT INTO user_auths (user_id, auth_type) +VALUES ($1, $2) +` + +type CreateUserAuthParams struct { + UserID int32 + AuthType string +} + +func (q *Queries) CreateUserAuth(ctx context.Context, arg CreateUserAuthParams) error { + _, err := q.db.Exec(ctx, createUserAuth, arg.UserID, arg.AuthType) + return err +} + const getGameByID = `-- name: GetGameByID :one SELECT game_id, game_type, state, display_name, duration_seconds, created_at, started_at, games.problem_id, problems.problem_id, title, description FROM games LEFT JOIN problems ON games.problem_id = problems.problem_id @@ -204,6 +232,20 @@ func (q *Queries) GetUserByID(ctx context.Context, userID int32) (User, error) { return i, err } +const isRegistrationTokenValid = `-- name: IsRegistrationTokenValid :one +SELECT EXISTS ( + SELECT 1 FROM registration_tokens + WHERE token = $1 +) +` + +func (q *Queries) IsRegistrationTokenValid(ctx context.Context, token string) (bool, error) { + row := q.db.QueryRow(ctx, isRegistrationTokenValid, token) + var exists bool + err := row.Scan(&exists) + return exists, err +} + const listGamePlayers = `-- name: ListGamePlayers :many SELECT game_id, game_players.user_id, users.user_id, username, display_name, icon_path, is_admin, created_at FROM game_players LEFT JOIN users ON game_players.user_id = users.user_id diff --git a/backend/fixtures/dev.sql b/backend/fixtures/dev.sql index 2daa8f5..9ba7f73 100644 --- a/backend/fixtures/dev.sql +++ b/backend/fixtures/dev.sql @@ -12,6 +12,9 @@ VALUES (2, 'password', '$2a$10$4Wl1M4jQs.GwkB4oT32KvuMQtF.EdqKuOc8z8KKOupnuMJRAVk32W'), (3, 'password', '$2a$10$F/TePpu1pyJRWgn0e6A14.VL9D/17sRxT/2DyZ2Oi4Eg/lR6n7JcK'); +INSERT INTO registration_tokens (token) +VALUES ('shah3Iheix6cheig'); + INSERT INTO problems (title, description) VALUES diff --git a/backend/query.sql b/backend/query.sql index f0e4034..13bbbe6 100644 --- a/backend/query.sql +++ b/backend/query.sql @@ -3,6 +3,11 @@ SELECT * FROM users WHERE users.user_id = $1 LIMIT 1; +-- name: CreateUser :one +INSERT INTO users (username, display_name, is_admin) +VALUES ($1, $1, false) +RETURNING user_id; + -- name: ListUsers :many SELECT * FROM users ORDER BY users.user_id; @@ -13,6 +18,16 @@ JOIN user_auths ON users.user_id = user_auths.user_id WHERE users.username = $1 LIMIT 1; +-- name: CreateUserAuth :exec +INSERT INTO user_auths (user_id, auth_type) +VALUES ($1, $2); + +-- name: IsRegistrationTokenValid :one +SELECT EXISTS ( + SELECT 1 FROM registration_tokens + WHERE token = $1 +); + -- name: ListGames :many SELECT * FROM games LEFT JOIN problems ON games.problem_id = problems.problem_id diff --git a/backend/schema.sql b/backend/schema.sql index 64642bc..2779eaf 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -17,6 +17,12 @@ CREATE TABLE user_auths ( ); CREATE INDEX idx_user_auths_user_id ON user_auths(user_id); +CREATE TABLE registration_tokens ( + registration_token_id SERIAL PRIMARY KEY, + token CHAR(16) NOT NULL +); +CREATE INDEX idx_registration_tokens_token ON registration_tokens(token); + CREATE TABLE problems ( problem_id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, |
