feat: initialize panel access before linking Minecraft accounts

Configure connection and storage before creating or resuming the one-time Owner login. Remove Minecraft prerequisites from setup and preserve established login credentials.

Let staff preview and confirm roles from configured authentication sources using the existing account-link storage and game UUID mapping. Retain in-game code proof for players, add client-version and lobby guidance, and support NodePort passkey origins.
This commit is contained in:
Lemon-miaow committed 2026-10-04 03:05:05 +08:00
1 parent 75845d8f58
commit efbbe27629
59 files changed
+1702 -1564

No files matched your search

+10
View File
@@ -62,6 +62,8 @@ type AuthSourceConfig struct {
Tag string `toml:"tag"`
Prefix string `toml:"prefix"`
URL string `toml:"url"`
// APIURL is optional for sources whose hasJoined URL does not use the standard path.
APIURL string `toml:"api_url"`
}
// SMTPConfig is the [smtp] table: the outbound mail relay felis-api delivers
@@ -149,6 +151,9 @@ type VelocityConfig struct {
// FELIS_GAME_PORT). The panel adds it to the server addresses players copy
// when it is not Minecraft's default; 0 means that default, 25565.
GamePort int `toml:"game_port"`
// GameVersion is the login/lobby protocol built by bootstrap. Empty means
// unknown for custom images; the panel must not guess a client version.
GameVersion string `toml:"game_version"`
}
// AuthConfig is the [auth] table: the two privileged faces and the Cloudflare
@@ -721,6 +726,11 @@ func (c *Config) validateAuthSources() error {
if problem := hasJoinedURLProblem(s.URL); problem != "" {
return fmt.Errorf("config: [[auth_source]] %q url %q %s", s.Tag, s.URL, problem)
}
if s.APIURL != "" {
if problem := hasJoinedURLProblem(s.APIURL); problem != "" {
return fmt.Errorf("config: [[auth_source]] %q api_url %q %s", s.Tag, s.APIURL, problem)
}
}
}
return nil
}
+22
View File
@@ -501,6 +501,28 @@ func TestLoadRejectsUnqueryableAuthSourceURL(t *testing.T) {
}
}
func TestAuthSourceProfileAPIURL(t *testing.T) {
for _, tc := range []struct {
url string
valid bool
}{
{"https://ygg.example.net/api/yggdrasil", true},
{"http://127.0.0.1:8080/ygg", true},
{"ygg.example.net/api", false},
{"http://ygg.example.net/api", false},
{"https://ygg.example.net/api?token=x", false},
} {
cfg, err := config.LoadNano(writeTOML(t, "[[auth_source]]\ntag = \"a\"\nprefix = \"AA\"\nurl = \"https://ygg.example.net/custom-check\"\napi_url = \""+tc.url+"\"\n"))
if tc.valid {
if err != nil || cfg.AuthSources[0].APIURL != tc.url {
t.Fatalf("api_url %q = %v, %v", tc.url, cfg, err)
}
} else if err == nil || !strings.Contains(err.Error(), "api_url") {
t.Fatalf("invalid api_url %q: %v", tc.url, err)
}
}
}
// A source reached over plaintext can be answered by anyone on the path, who can then log in
// as any player of that source. Only a same-host or private-network root may skip TLS, and
// that is decided on the literal host, since nothing is resolved at load time.