mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +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.
158 lines
4.0 KiB
Go
158 lines
4.0 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
// mockAPIUserRepo implements APIUserRepo for testing.
|
|
type mockAPIUserRepo struct {
|
|
registerID uint32
|
|
registerRights uint32
|
|
registerErr error
|
|
|
|
credentialsID uint32
|
|
credentialsPassword string
|
|
credentialsRights uint32
|
|
credentialsErr error
|
|
|
|
lastLogin time.Time
|
|
lastLoginErr error
|
|
|
|
returnExpiry time.Time
|
|
returnExpiryErr error
|
|
|
|
updateReturnExpiryErr error
|
|
updateLastLoginErr error
|
|
}
|
|
|
|
func (m *mockAPIUserRepo) Register(_ context.Context, _, _ string, _ time.Time) (uint32, uint32, error) {
|
|
return m.registerID, m.registerRights, m.registerErr
|
|
}
|
|
|
|
func (m *mockAPIUserRepo) GetCredentials(_ context.Context, _ string) (uint32, string, uint32, error) {
|
|
return m.credentialsID, m.credentialsPassword, m.credentialsRights, m.credentialsErr
|
|
}
|
|
|
|
func (m *mockAPIUserRepo) GetLastLogin(_ uint32) (time.Time, error) {
|
|
return m.lastLogin, m.lastLoginErr
|
|
}
|
|
|
|
func (m *mockAPIUserRepo) GetReturnExpiry(_ uint32) (time.Time, error) {
|
|
return m.returnExpiry, m.returnExpiryErr
|
|
}
|
|
|
|
func (m *mockAPIUserRepo) UpdateReturnExpiry(_ uint32, _ time.Time) error {
|
|
return m.updateReturnExpiryErr
|
|
}
|
|
|
|
func (m *mockAPIUserRepo) UpdateLastLogin(_ uint32, _ time.Time) error {
|
|
return m.updateLastLoginErr
|
|
}
|
|
|
|
// mockAPICharacterRepo implements APICharacterRepo for testing.
|
|
type mockAPICharacterRepo struct {
|
|
newCharacter Character
|
|
newCharacterErr error
|
|
|
|
countForUser int
|
|
countForUserErr error
|
|
|
|
createChar Character
|
|
createCharErr error
|
|
|
|
isNewResult bool
|
|
isNewErr error
|
|
|
|
hardDeleteErr error
|
|
softDeleteErr error
|
|
|
|
characters []Character
|
|
charactersErr error
|
|
|
|
exportResult map[string]interface{}
|
|
exportErr error
|
|
|
|
grantImportTokenErr error
|
|
revokeImportTokenErr error
|
|
importSaveErr error
|
|
}
|
|
|
|
func (m *mockAPICharacterRepo) GetNewCharacter(_ context.Context, _ uint32) (Character, error) {
|
|
return m.newCharacter, m.newCharacterErr
|
|
}
|
|
|
|
func (m *mockAPICharacterRepo) CountForUser(_ context.Context, _ uint32) (int, error) {
|
|
return m.countForUser, m.countForUserErr
|
|
}
|
|
|
|
func (m *mockAPICharacterRepo) Create(_ context.Context, _ uint32, _ uint32) (Character, error) {
|
|
return m.createChar, m.createCharErr
|
|
}
|
|
|
|
func (m *mockAPICharacterRepo) IsNew(_ uint32) (bool, error) {
|
|
return m.isNewResult, m.isNewErr
|
|
}
|
|
|
|
func (m *mockAPICharacterRepo) HardDelete(_ uint32) error {
|
|
return m.hardDeleteErr
|
|
}
|
|
|
|
func (m *mockAPICharacterRepo) SoftDelete(_ uint32) error {
|
|
return m.softDeleteErr
|
|
}
|
|
|
|
func (m *mockAPICharacterRepo) GetForUser(_ context.Context, _ uint32) ([]Character, error) {
|
|
return m.characters, m.charactersErr
|
|
}
|
|
|
|
func (m *mockAPICharacterRepo) ExportSave(_ context.Context, _, _ uint32) (map[string]interface{}, error) {
|
|
return m.exportResult, m.exportErr
|
|
}
|
|
|
|
func (m *mockAPICharacterRepo) GrantImportToken(_ context.Context, _, _ uint32, _ string, _ time.Time) error {
|
|
return m.grantImportTokenErr
|
|
}
|
|
|
|
func (m *mockAPICharacterRepo) RevokeImportToken(_ context.Context, _, _ uint32) error {
|
|
return m.revokeImportTokenErr
|
|
}
|
|
|
|
func (m *mockAPICharacterRepo) ImportSave(_ context.Context, _, _ uint32, _ string, _ SaveBlobs) error {
|
|
return m.importSaveErr
|
|
}
|
|
|
|
// mockAPIEventRepo implements APIEventRepo for testing.
|
|
type mockAPIEventRepo struct {
|
|
featureWeapon *FeatureWeaponRow
|
|
featureWeaponErr error
|
|
|
|
events []EventRow
|
|
eventsErr error
|
|
}
|
|
|
|
func (m *mockAPIEventRepo) GetFeatureWeapon(_ context.Context, _ time.Time) (*FeatureWeaponRow, error) {
|
|
return m.featureWeapon, m.featureWeaponErr
|
|
}
|
|
|
|
func (m *mockAPIEventRepo) GetActiveEvents(_ context.Context, _ string) ([]EventRow, error) {
|
|
return m.events, m.eventsErr
|
|
}
|
|
|
|
// mockAPISessionRepo implements APISessionRepo for testing.
|
|
type mockAPISessionRepo struct {
|
|
createTokenID uint32
|
|
createTokenErr error
|
|
|
|
userID uint32
|
|
userIDErr error
|
|
}
|
|
|
|
func (m *mockAPISessionRepo) CreateToken(_ context.Context, _ uint32, _ string) (uint32, error) {
|
|
return m.createTokenID, m.createTokenErr
|
|
}
|
|
|
|
func (m *mockAPISessionRepo) GetUserIDByToken(_ context.Context, _ string) (uint32, error) {
|
|
return m.userID, m.userIDErr
|
|
}
|