Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
GOOSE_DRIVER="sqlite3"
GOOSE_DBSTRING="./database/main.db"
GOOSE_MIGRATION_DIR="./database/migrations"
GOOSE_MIGRATION_DIR="./database/migrations"

SERVER_ADDRESS=":8080"
JWT_SECRET=""
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,4 @@ go.work.sum
.env

# Editor/IDE
.idea/
.vscode/
.idea/
10 changes: 10 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"material-icon-theme.folders.associations": {
"internal": "src",
"port": "interface",
"usecase": "controller",
"queries": "scripts",
"driven": "connection",
"driver": "trigger"
}
}
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,25 @@ Nouns:
auth validate-passwd <password>
hash-passwd <password>
check-passwd <username> <password>
data games <player id>
fetch games <player id>
shared <game id>
interactions <game id> <limit>
session save <game id> <player id> <duration in seconds>
command save <game id> <player id> <duration in seconds>
resume <game id> <player id>
pause <game id> <player id>
```

## HTTP API

```text
no auth required:
POST /login username + password → set cookies

auth/cookies required:
GET /games game access → list accessible games
GET /shared game ID → current shared state
GET /interactions game ID → latest interactions
POST /save game ID → add save interaction
POST /save game ID + duration in seconds → add save interaction
POST /play game ID → start or resume the game
POST /pause game ID → pause the game
```
53 changes: 45 additions & 8 deletions api/openai.yaml → api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ openapi: 3.1.0

info:
title: Keep It Up API
version: 1.0.0
version: 1.1.0

paths:
/login:
Expand Down Expand Up @@ -91,6 +91,12 @@ paths:
- sessionCookie: []
parameters:
- $ref: '#/components/parameters/GameId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SaveRequest'
responses:
'204':
description: Save interaction added.
Expand All @@ -110,7 +116,7 @@ paths:
- $ref: '#/components/parameters/GameId'
responses:
'204':
description: Game started or resumed.
description: Game resumed.
'401':
$ref: '#/components/responses/Unauthorized'
'404':
Expand Down Expand Up @@ -141,6 +147,8 @@ components:
type: apiKey
in: cookie
name: session
schema:
type: string

parameters:
GameId:
Expand Down Expand Up @@ -186,14 +194,22 @@ components:
SharedData:
type: object
required:
- gameId
- status
properties:
gameId:
type: integer
format: int64
status:
type: string
enum:
- not_started
- playing
- paused
valid:
type:
- boolean
- 'null'
deadlineAt:
type:
- string
Expand All @@ -213,26 +229,47 @@ components:
format: date-time
description: The time at which the shared state was last paused.

SaveRequest:
type: object
required:
- duration
properties:
duration:
type: integer
format: int64
minimum: 1
description: Duration of the save, in seconds.

Interaction:
type: object
required:
- id
- playerId
- gameId
- action
- occurredAt
properties:
id:
type: integer
format: int64
playerId:
gameId:
type: integer
format: int64
playerId:
type:
- integer
- 'null'
format: int64
action:
type: string
enum:
- save
- play
- pause
- saved
- paused
- resumed
occurredAt:
type: string
format: date-time
format: date-time
savedBy:
type:
- integer
- 'null'
format: int64
13 changes: 7 additions & 6 deletions cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import (
"syscall"

"keep-it-up/internal/application/usecase"
"keep-it-up/internal/infrastructure/constant"
"keep-it-up/internal/infrastructure/database"
"keep-it-up/internal/infrastructure/driven"
clidriver "keep-it-up/internal/infrastructure/driver"
"keep-it-up/internal/infrastructure/driver/cliadapter"
"keep-it-up/internal/infrastructure/util"

_ "modernc.org/sqlite"
Expand All @@ -27,7 +28,7 @@ func run() int {
defer stop()

// --- Driven side: infrastructure dependencies ---------------------
util.LoadEnv(".env")
util.LoadEnv(constant.EnvFilename)

dbString, err := filepath.Abs(os.Getenv("GOOSE_DBSTRING"))
if err != nil {
Expand All @@ -52,20 +53,20 @@ func run() int {
q := database.New(sqlDB)

// --- Application side: use cases implementing the driver ports ----
auth := usecase.NewAuthentication(q)
deps := clidriver.Deps{
auth := usecase.NewAuthentication(q, nil)
deps := cliadapter.Deps{
Games: usecase.NewGameManagement(q),
Access: usecase.NewAccessManagement(q),
Players: usecase.NewPlayerManagement(q, auth),
Auth: auth,
Data: usecase.NewDataFetching(q),
Fetch: usecase.NewDataFetching(q),
Commands: usecase.NewGameCommands(q, &driven.DefaultTimeProvider{}),
Stdout: os.Stdout,
Stderr: os.Stderr,
}

// --- Driver side: CLI adapter --------------------------------------
cli := clidriver.New(deps)
cli := cliadapter.New(deps)

if err := cli.Run(ctx, os.Args[1:]); err != nil {
fmt.Println(err)
Expand Down
116 changes: 107 additions & 9 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,119 @@
package main

import (
"github.com/labstack/echo"
"context"
"database/sql"
"errors"
"fmt"
"keep-it-up/internal/application/usecase"
"keep-it-up/internal/infrastructure/constant"
"keep-it-up/internal/infrastructure/database"
"keep-it-up/internal/infrastructure/driven"
"keep-it-up/internal/infrastructure/driver/httpadapter"
"keep-it-up/internal/infrastructure/util"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"sync"

_ "modernc.org/sqlite"
)

func main() {
e := echo.New()
os.Exit(run())
}

func run() int {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

util.LoadEnv(constant.EnvFilename)

jwtSecret := os.Getenv("JWT_SECRET")
if jwtSecret == "" {
log.Println("failed to get JWT secret")
return 1
}

addr := os.Getenv("SERVER_ADDRESS")
if addr == "" {
log.Println("failed to get server address")
}

dbString, err := filepath.Abs(os.Getenv("GOOSE_DBSTRING"))
if err != nil {
fmt.Printf("failed to get dbstring absolute path: %v\n", err)
return 1
}

sqlDB, err := sql.Open("sqlite", fmt.Sprintf(
"file:%s?mode=rw",
dbString,
))
if err != nil {
fmt.Fprintf(os.Stderr, "open database: %v\n", err)
return 1
}
defer sqlDB.Close()
if err := sqlDB.PingContext(ctx); err != nil {
fmt.Fprintf(os.Stderr, "connect database: %v\n", err)
return 1
}

q := database.New(sqlDB)

timeProvider := &driven.DefaultTimeProvider{}
deps := httpadapter.Deps{
Auth: usecase.NewAuthentication(
q,
&driven.JwtTokenGenerator{
JwtSecret: jwtSecret,
TimeProvider: timeProvider,
},
),
}

adapter := httpadapter.New(
addr,
jwtSecret,
timeProvider,
deps,
)

e.GET("/", func(ctx echo.Context) error {
return ctx.JSON(
http.StatusNoContent,
map[string]string{"message": "Not implemented yet!"},
)
var wg sync.WaitGroup
errCh := make(chan error, 1)

wg.Go(func() {
log.Printf("Starting server on %s...\n", addr)
if err := adapter.Run(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
wrappedErr := fmt.Errorf(
"server failed to start or stopped unexpectedly: %w",
err,
)
errCh <- wrappedErr
stop()
return
}

log.Println("server stopped gracefully")
})

if err := e.Start(":8080"); err != nil {
e.Logger.Error("failed to start server", "error", err)
<-ctx.Done()
log.Printf("Shutdown requested: %v", ctx.Err())

wg.Wait()
close(errCh)

select {
case err := <-errCh:
if err != nil {
log.Printf("shutdown after server failure: %v", err)
return 1
}
default:
}

return 0
}
9 changes: 4 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ tool (
)

require (
github.com/labstack/echo v3.3.10+incompatible
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/labstack/echo-jwt/v5 v5.0.2
github.com/labstack/echo/v5 v5.2.1
github.com/pressly/goose/v3 v3.27.3
golang.org/x/crypto v0.54.0
modernc.org/sqlite v1.54.0
Expand Down Expand Up @@ -47,8 +49,6 @@ require (
github.com/joho/godotenv v1.5.1 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/klauspost/compress v1.19.1 // indirect
github.com/labstack/gommon v0.5.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.23 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/mfridman/xflag v0.1.0 // indirect
Expand Down Expand Up @@ -76,8 +76,6 @@ require (
github.com/sqlc-dev/sqlc v1.31.1 // indirect
github.com/tetratelabs/wazero v1.11.0 // indirect
github.com/tursodatabase/libsql-client-go v0.0.0-20260528064733-9d5d30a29a60 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vertica/vertica-sql-go v1.3.8 // indirect
github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 // indirect
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect
Expand All @@ -95,6 +93,7 @@ require (
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260810153831-ec0a7760b754 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260807164820-c8921c73eeea // indirect
google.golang.org/grpc v1.82.1 // indirect
Expand Down
Loading
Loading