mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
Adds two complementary paths for transferring character save data between
Erupe instances without breaking the SHA-256 integrity check system:
- `cmd/saveutil/`: admin CLI with `import`, `export`, `grant-import`, and
`revoke-import` subcommands. Direct DB access; no server running required.
- `POST /v2/characters/{id}/import`: player-facing API endpoint gated behind
a one-time token issued by `saveutil grant-import` (default TTL 24 h).
Token is validated and consumed atomically to prevent TOCTOU races.
- Migration `0013_save_transfer`: `savedata_import_token` and
`savedata_import_token_expiry` columns on `characters` table.
- Both paths decompress incoming savedata and recompute the SHA-256 hash
server-side, so the integrity check remains valid after import.
- README documents both methods and the per-character hash-reset workaround.
Closes #183.
104 lines
4.5 KiB
Go
104 lines
4.5 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
// Repository interfaces decouple API server business logic from concrete
|
|
// PostgreSQL implementations, enabling mock/stub injection for unit tests.
|
|
|
|
// SaveBlobs holds the transferable save data columns for a character.
|
|
// SavedataHash must be set by the caller (SHA-256 of decompressed Savedata).
|
|
type SaveBlobs struct {
|
|
Savedata []byte
|
|
SavedataHash []byte
|
|
Decomyset []byte
|
|
Hunternavi []byte
|
|
Otomoairou []byte
|
|
Partner []byte
|
|
Platebox []byte
|
|
Platedata []byte
|
|
Platemyset []byte
|
|
Rengokudata []byte
|
|
Savemercenary []byte
|
|
GachaItems []byte
|
|
HouseInfo []byte
|
|
LoginBoost []byte
|
|
SkinHist []byte
|
|
Scenariodata []byte
|
|
Savefavoritequest []byte
|
|
Mezfes []byte
|
|
}
|
|
|
|
// APIUserRepo defines the contract for user-related data access.
|
|
type APIUserRepo interface {
|
|
// Register creates a new user and returns their ID and rights.
|
|
Register(ctx context.Context, username, passwordHash string, returnExpires time.Time) (id uint32, rights uint32, err error)
|
|
// GetCredentials returns the user's ID, password hash, and rights.
|
|
GetCredentials(ctx context.Context, username string) (id uint32, passwordHash string, rights uint32, err error)
|
|
// GetLastLogin returns the user's last login time.
|
|
GetLastLogin(uid uint32) (time.Time, error)
|
|
// GetReturnExpiry returns the user's return expiry time.
|
|
GetReturnExpiry(uid uint32) (time.Time, error)
|
|
// UpdateReturnExpiry sets the user's return expiry time.
|
|
UpdateReturnExpiry(uid uint32, expiry time.Time) error
|
|
// UpdateLastLogin sets the user's last login time.
|
|
UpdateLastLogin(uid uint32, loginTime time.Time) error
|
|
}
|
|
|
|
// APICharacterRepo defines the contract for character-related data access.
|
|
type APICharacterRepo interface {
|
|
// GetNewCharacter returns an existing new (unfinished) character for a user.
|
|
GetNewCharacter(ctx context.Context, userID uint32) (Character, error)
|
|
// CountForUser returns the total number of characters for a user.
|
|
CountForUser(ctx context.Context, userID uint32) (int, error)
|
|
// Create inserts a new character and returns it.
|
|
Create(ctx context.Context, userID uint32, lastLogin uint32) (Character, error)
|
|
// IsNew returns whether a character is a new (unfinished) character.
|
|
IsNew(charID uint32) (bool, error)
|
|
// HardDelete permanently removes a character.
|
|
HardDelete(charID uint32) error
|
|
// SoftDelete marks a character as deleted.
|
|
SoftDelete(charID uint32) error
|
|
// GetForUser returns all finalized (non-deleted) characters for a user.
|
|
GetForUser(ctx context.Context, userID uint32) ([]Character, error)
|
|
// ExportSave returns the full character row as a map.
|
|
ExportSave(ctx context.Context, userID, charID uint32) (map[string]interface{}, error)
|
|
// GrantImportToken sets a one-time import token for a character owned by userID.
|
|
GrantImportToken(ctx context.Context, charID, userID uint32, token string, expiry time.Time) error
|
|
// RevokeImportToken clears any pending import token for a character owned by userID.
|
|
RevokeImportToken(ctx context.Context, charID, userID uint32) error
|
|
// ImportSave atomically validates+consumes the import token and writes all save blobs.
|
|
// Returns an error if the token is invalid, expired, or the character doesn't belong to userID.
|
|
ImportSave(ctx context.Context, charID, userID uint32, token string, blobs SaveBlobs) error
|
|
}
|
|
|
|
// APIEventRepo defines the contract for read-only event data access.
|
|
type APIEventRepo interface {
|
|
// GetFeatureWeapon returns the feature weapon entry for the given week start time.
|
|
GetFeatureWeapon(ctx context.Context, startTime time.Time) (*FeatureWeaponRow, error)
|
|
// GetActiveEvents returns all events of the given type.
|
|
GetActiveEvents(ctx context.Context, eventType string) ([]EventRow, error)
|
|
}
|
|
|
|
// FeatureWeaponRow holds a single feature_weapon table row.
|
|
type FeatureWeaponRow struct {
|
|
StartTime time.Time `db:"start_time"`
|
|
ActiveFeatures uint32 `db:"featured"`
|
|
}
|
|
|
|
// EventRow holds a single events table row with epoch start time.
|
|
type EventRow struct {
|
|
ID int `db:"id"`
|
|
StartTime int64 `db:"start_time"`
|
|
}
|
|
|
|
// APISessionRepo defines the contract for session/token data access.
|
|
type APISessionRepo interface {
|
|
// CreateToken inserts a new sign session and returns its ID and token.
|
|
CreateToken(ctx context.Context, uid uint32, token string) (tokenID uint32, err error)
|
|
// GetUserIDByToken returns the user ID for a given session token.
|
|
GetUserIDByToken(ctx context.Context, token string) (uint32, error)
|
|
}
|