aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-08-17 19:35:56 +0900
committernsfisis <nsfisis@gmail.com>2024-08-17 21:10:18 +0900
commit95dd5f0c346d10c9a9497daf88cd199ce71c8122 (patch)
tree6d3621b22cc44d7e93bc6a46b300479619d240d2 /backend
parent48a70e06f54d81cdc6b0c22c2dcce8426b86decf (diff)
downloadiosdc-japan-2024-albatross-95dd5f0c346d10c9a9497daf88cd199ce71c8122.tar.gz
iosdc-japan-2024-albatross-95dd5f0c346d10c9a9497daf88cd199ce71c8122.tar.zst
iosdc-japan-2024-albatross-95dd5f0c346d10c9a9497daf88cd199ce71c8122.zip
feat(backend): fetch user icon from fortee
Diffstat (limited to 'backend')
-rw-r--r--backend/account/icon.go85
-rw-r--r--backend/admin/handler.go26
-rw-r--r--backend/admin/templates/users.html3
-rw-r--r--backend/auth/auth.go9
-rw-r--r--backend/db/query.sql.go16
-rw-r--r--backend/fortee/fortee.go6
-rw-r--r--backend/main.go9
-rw-r--r--backend/query.sql5
8 files changed, 156 insertions, 3 deletions
diff --git a/backend/account/icon.go b/backend/account/icon.go
new file mode 100644
index 0000000..40632c6
--- /dev/null
+++ b/backend/account/icon.go
@@ -0,0 +1,85 @@
+package account
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "time"
+
+ "github.com/nsfisis/iosdc-japan-2024-albatross/backend/db"
+ "github.com/nsfisis/iosdc-japan-2024-albatross/backend/fortee"
+)
+
+func FetchIcon(
+ ctx context.Context,
+ q *db.Queries,
+ userID int,
+) error {
+ ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
+ defer cancel()
+
+ // Fetch user.
+ user, err := q.GetUserByID(ctx, int32(userID))
+ if err != nil {
+ return fmt.Errorf("failed to fetch user icon (uid=%d): %w", userID, err)
+ }
+ // Fetch user icon URL.
+ avatarURL, err := fortee.GetUserAvatarURL(ctx, user.Username)
+ if err != nil {
+ return fmt.Errorf("failed to fetch user icon (uid=%d): %w", userID, err)
+ }
+ // Download user icon file.
+ filePath := fmt.Sprintf("/files/%s/icon%s", url.PathEscape(user.Username), path.Ext(avatarURL))
+ if err := downloadFile(ctx, fortee.Endpoint+avatarURL, "/data"+filePath); err != nil {
+ return fmt.Errorf("failed to fetch user icon (uid=%d): %w", userID, err)
+ }
+ // Save user icon path.
+ if err := q.UpdateUserIconPath(ctx, db.UpdateUserIconPathParams{
+ UserID: int32(userID),
+ IconPath: &filePath,
+ }); err != nil {
+ return fmt.Errorf("failed to fetch user icon (uid=%d): %w", userID, err)
+ }
+ return nil
+}
+
+func downloadFile(ctx context.Context, url string, filePath string) error {
+ req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+ if err != nil {
+ return fmt.Errorf("failed to download file (%s): %w", url, err)
+ }
+
+ client := &http.Client{}
+ res, err := client.Do(req)
+ if err != nil {
+ return fmt.Errorf("failed to download file (%s): %w", url, err)
+ }
+ defer res.Body.Close()
+
+ if res.StatusCode != http.StatusOK {
+ return fmt.Errorf("failed to download file (%s): status %d", url, res.StatusCode)
+ }
+
+ fileDir := filepath.Dir(filePath)
+ if err := os.MkdirAll(fileDir, 0755); err != nil {
+ return fmt.Errorf("failed to create directory (%s): %w", fileDir, err)
+ }
+
+ file, err := os.Create(filePath)
+ if err != nil {
+ return fmt.Errorf("failed to open file (%s): %w", filePath, err)
+ }
+ defer file.Close()
+
+ _, err = io.Copy(file, res.Body)
+ if err != nil {
+ return fmt.Errorf("failed to save file (%s): %w", filePath, err)
+ }
+
+ return nil
+}
diff --git a/backend/admin/handler.go b/backend/admin/handler.go
index 41eacd4..1b60de5 100644
--- a/backend/admin/handler.go
+++ b/backend/admin/handler.go
@@ -1,7 +1,9 @@
package admin
import (
+ "context"
"errors"
+ "log"
"net/http"
"strconv"
"time"
@@ -10,6 +12,7 @@ import (
"github.com/jackc/pgx/v5/pgtype"
"github.com/labstack/echo/v4"
+ "github.com/nsfisis/iosdc-japan-2024-albatross/backend/account"
"github.com/nsfisis/iosdc-japan-2024-albatross/backend/auth"
"github.com/nsfisis/iosdc-japan-2024-albatross/backend/db"
)
@@ -58,6 +61,7 @@ func (h *Handler) RegisterHandlers(g *echo.Group) {
g.GET("/dashboard", h.getDashboard)
g.GET("/users", h.getUsers)
g.GET("/users/:userID", h.getUserEdit)
+ g.POST("/users/:userID/fetch-icon", h.postUserFetchIcon)
g.GET("/games", h.getGames)
g.GET("/games/:gameID", h.getGameEdit)
g.POST("/games/:gameID", h.postGameEdit)
@@ -116,6 +120,28 @@ func (h *Handler) getUserEdit(c echo.Context) error {
})
}
+func (h *Handler) postUserFetchIcon(c echo.Context) error {
+ userID, err := strconv.Atoi(c.Param("userID"))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusBadRequest, "Invalid user id")
+ }
+ row, err := h.q.GetUserByID(c.Request().Context(), int32(userID))
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return echo.NewHTTPError(http.StatusNotFound)
+ }
+ return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+ go func() {
+ err := account.FetchIcon(context.Background(), h.q, int(row.UserID))
+ if err != nil {
+ log.Printf("%v", err)
+ // The failure is intentionally ignored. Retry manually if needed.
+ }
+ }()
+ return c.Redirect(http.StatusSeeOther, "/iosdc-japan/2024/code-battle/admin/users")
+}
+
func (h *Handler) getGames(c echo.Context) error {
rows, err := h.q.ListGames(c.Request().Context())
if err != nil {
diff --git a/backend/admin/templates/users.html b/backend/admin/templates/users.html
index 3543457..3dc03c0 100644
--- a/backend/admin/templates/users.html
+++ b/backend/admin/templates/users.html
@@ -11,6 +11,9 @@
<a href="/iosdc-japan/2024/code-battle/admin/users/{{ .UserID }}">
{{ .DisplayName }} (id={{ .UserID }} username={{ .Username }}){{ if .IsAdmin }} <em>admin</em>{{ end }}
</a>
+ <form method="post" action="/iosdc-japan/2024/code-battle/admin/users/{{ .UserID }}/fetch-icon">
+ <button type="submit">Fetch Icon</button>
+ </form>
</li>
{{ end }}
</ul>
diff --git a/backend/auth/auth.go b/backend/auth/auth.go
index 10906f5..2266c50 100644
--- a/backend/auth/auth.go
+++ b/backend/auth/auth.go
@@ -3,11 +3,13 @@ package auth
import (
"context"
"errors"
+ "log"
"time"
"github.com/jackc/pgx/v5"
"golang.org/x/crypto/bcrypt"
+ "github.com/nsfisis/iosdc-japan-2024-albatross/backend/account"
"github.com/nsfisis/iosdc-japan-2024-albatross/backend/db"
"github.com/nsfisis/iosdc-japan-2024-albatross/backend/fortee"
)
@@ -98,6 +100,13 @@ func signup(
}); err != nil {
return 0, err
}
+ go func() {
+ err := account.FetchIcon(context.Background(), queries, int(userID))
+ if err != nil {
+ log.Printf("%v", err)
+ // The failure is intentionally ignored. Retry manually if needed.
+ }
+ }()
return int(userID), nil
}
diff --git a/backend/db/query.sql.go b/backend/db/query.sql.go
index 02e660a..35b7edd 100644
--- a/backend/db/query.sql.go
+++ b/backend/db/query.sql.go
@@ -584,3 +584,19 @@ func (q *Queries) UpdateGameState(ctx context.Context, arg UpdateGameStateParams
_, err := q.db.Exec(ctx, updateGameState, arg.GameID, arg.State)
return err
}
+
+const updateUserIconPath = `-- name: UpdateUserIconPath :exec
+UPDATE users
+SET icon_path = $2
+WHERE user_id = $1
+`
+
+type UpdateUserIconPathParams struct {
+ UserID int32
+ IconPath *string
+}
+
+func (q *Queries) UpdateUserIconPath(ctx context.Context, arg UpdateUserIconPathParams) error {
+ _, err := q.db.Exec(ctx, updateUserIconPath, arg.UserID, arg.IconPath)
+ return err
+}
diff --git a/backend/fortee/fortee.go b/backend/fortee/fortee.go
index 79ba065..cfe4eff 100644
--- a/backend/fortee/fortee.go
+++ b/backend/fortee/fortee.go
@@ -7,7 +7,7 @@ import (
)
const (
- apiEndpoint = "https://fortee.jp"
+ Endpoint = "https://fortee.jp"
)
var (
@@ -16,7 +16,7 @@ var (
)
func Login(ctx context.Context, username string, password string) (string, error) {
- client, err := NewClientWithResponses(apiEndpoint, WithRequestEditorFn(addAcceptHeader))
+ client, err := NewClientWithResponses(Endpoint, WithRequestEditorFn(addAcceptHeader))
if err != nil {
return "", err
}
@@ -41,7 +41,7 @@ func Login(ctx context.Context, username string, password string) (string, error
}
func GetUserAvatarURL(ctx context.Context, username string) (string, error) {
- client, err := NewClientWithResponses(apiEndpoint, WithRequestEditorFn(addAcceptHeader))
+ client, err := NewClientWithResponses(Endpoint, WithRequestEditorFn(addAcceptHeader))
if err != nil {
return "", err
}
diff --git a/backend/main.go b/backend/main.go
index 3296957..890e666 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -96,6 +96,15 @@ func main() {
return c.Redirect(http.StatusPermanentRedirect, "http://localhost:5173/iosdc-japan/2024/code-battle/logout")
})
+ // For local dev: This is never used in production because the reverse
+ // proxy directly handles /files.
+ filesGroup := e.Group("/iosdc-japan/2024/code-battle/files")
+ filesGroup.Use(middleware.StaticWithConfig(middleware.StaticConfig{
+ Root: "/",
+ Filesystem: http.Dir("/data/files"),
+ IgnoreBase: true,
+ }))
+
go gameHubs.Run()
go func() {
diff --git a/backend/query.sql b/backend/query.sql
index 6b0ecdd..dc5f384 100644
--- a/backend/query.sql
+++ b/backend/query.sql
@@ -13,6 +13,11 @@ INSERT INTO users (username, display_name, is_admin)
VALUES ($1, $1, false)
RETURNING user_id;
+-- name: UpdateUserIconPath :exec
+UPDATE users
+SET icon_path = $2
+WHERE user_id = $1;
+
-- name: ListUsers :many
SELECT * FROM users
ORDER BY users.user_id;