Stop ignoring keyring.Open failures in all three login files. A failed Open previously left a nil ring behind the blank identifier, so the next Get/Set panicked with no message. Each service now opens its ring in init, logs a service-prefixed warning when storage is unavailable, and guards every use with a Ready check that fails safe to logged-out. - AniListUserFunctions.go: add aniRingReady/aniRingSet helpers, log Open and per-key Set failures, reject invalid ExpiresIn instead of silently storing 0, clear in-memory JWT on logout even when storage is missing - MALUserFunctions.go: add malRingReady/malRingSet helpers covering login, OAuth callback, and token-refresh saves; same Open/Set/ ExpiresIn/logout treatment - SimklUserFunctions.go: add simklRingReady/simklRingSet helpers; same Open/Set/logout treatment No wallet, key names, or login flow changed. Same ServiceName AniTrack, same keys, same OAuth callback behavior.
258 lines
7.1 KiB
Go
258 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/99designs/keyring"
|
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
)
|
|
|
|
var aniListJwt AniListJWT
|
|
|
|
var aniRing keyring.Keyring
|
|
|
|
func init() {
|
|
var err error
|
|
aniRing, err = keyring.Open(keyring.Config{
|
|
ServiceName: "AniTrack",
|
|
KeychainName: "AniTrack",
|
|
KeychainSynchronizable: false,
|
|
KeychainTrustApplication: true,
|
|
KeychainAccessibleWhenUnlocked: true,
|
|
})
|
|
if err != nil {
|
|
log.Printf("anilist: secure storage unavailable: %s", err)
|
|
}
|
|
}
|
|
|
|
func aniRingReady() bool {
|
|
if aniRing == nil {
|
|
log.Println("anilist: secure storage unavailable")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func aniRingSet(key string, data []byte) error {
|
|
if !aniRingReady() {
|
|
return errors.New("anilist: secure storage unavailable")
|
|
}
|
|
if err := aniRing.Set(keyring.Item{Key: key, Data: data}); err != nil {
|
|
log.Printf("anilist: save %s failed: %s", key, err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var aniCtxShutdown, aniCancel = context.WithCancel(context.Background())
|
|
|
|
func (a *App) CheckIfAniListLoggedIn() bool {
|
|
if (AniListJWT{} == aniListJwt) {
|
|
if !aniRingReady() {
|
|
return false
|
|
}
|
|
tokenType, tokenErr := aniRing.Get("anilistTokenType")
|
|
expiresIn, expiresInErr := aniRing.Get("anilistTokenExpiresIn")
|
|
refreshToken, refreshTokenErr := aniRing.Get("anilistRefreshToken")
|
|
accessToken, accessTokenErr := aniRing.Get("anilistAccessToken")
|
|
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
|
|
return false
|
|
} else {
|
|
var expiresInConvertErr error
|
|
aniListJwt.TokenType = string(tokenType.Data)
|
|
aniListJwt.AccessToken = string(accessToken.Data)
|
|
aniListJwt.RefreshToken = string(refreshToken.Data)
|
|
aniListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data))
|
|
if expiresInConvertErr != nil {
|
|
log.Printf("anilist: invalid expiresIn %q: %s", string(expiresIn.Data), expiresInConvertErr)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
func (a *App) AniListLogin() {
|
|
if (AniListJWT{} == aniListJwt) {
|
|
if !aniRingReady() {
|
|
log.Println("anilist: cannot check login, secure storage unavailable")
|
|
return
|
|
}
|
|
tokenType, tokenErr := aniRing.Get("anilistTokenType")
|
|
expiresIn, expiresInErr := aniRing.Get("anilistTokenExpiresIn")
|
|
refreshToken, refreshTokenErr := aniRing.Get("anilistRefreshToken")
|
|
accessToken, accessTokenErr := aniRing.Get("anilistAccessToken")
|
|
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
|
|
getAniListCodeUrl := "https://anilist.co/api/v2/oauth/authorize?client_id=" + Environment.ANILIST_APP_ID + "&redirect_uri=" + Environment.ANILIST_CALLBACK_URI + "&response_type=code"
|
|
runtime.BrowserOpenURL(*wailsContext, getAniListCodeUrl)
|
|
|
|
serverDone := &sync.WaitGroup{}
|
|
serverDone.Add(1)
|
|
a.handleAniListCallback(serverDone)
|
|
serverDone.Wait()
|
|
} else {
|
|
var expiresInConvertErr error
|
|
aniListJwt.TokenType = string(tokenType.Data)
|
|
aniListJwt.AccessToken = string(accessToken.Data)
|
|
aniListJwt.RefreshToken = string(refreshToken.Data)
|
|
aniListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data))
|
|
if expiresInConvertErr != nil {
|
|
log.Printf("anilist: invalid expiresIn %q: %s", string(expiresIn.Data), expiresInConvertErr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *App) handleAniListCallback(wg *sync.WaitGroup) {
|
|
mux := http.NewServeMux()
|
|
srv := &http.Server{Addr: ":6734", Handler: mux}
|
|
mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
|
|
select {
|
|
case <-aniCtxShutdown.Done():
|
|
fmt.Println("Shutting down...")
|
|
return
|
|
default:
|
|
}
|
|
content := r.FormValue("code")
|
|
if content != "" {
|
|
aniListJwt = getAniListAuthorizationToken(content)
|
|
_ = aniRingSet("anilistTokenType", []byte(aniListJwt.TokenType))
|
|
_ = aniRingSet("anilistTokenExpiresIn", []byte(strconv.Itoa(aniListJwt.ExpiresIn)))
|
|
_ = aniRingSet("anilistAccessToken", []byte(aniListJwt.AccessToken))
|
|
_ = aniRingSet("anilistRefreshToken", []byte(aniListJwt.RefreshToken))
|
|
_, err := runtime.MessageDialog(*wailsContext, runtime.MessageDialogOptions{
|
|
Title: "AniList Authorization",
|
|
Message: "It is now safe to close your browser tab",
|
|
})
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
fmt.Println("Shutting down...")
|
|
aniCancel()
|
|
err = srv.Shutdown(context.Background())
|
|
if err != nil {
|
|
log.Println("server.Shutdown:", err)
|
|
}
|
|
} else {
|
|
_, err := fmt.Fprintf(w, "Getting code failed.")
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
})
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
log.Fatalf("listen: %s\n", err)
|
|
}
|
|
fmt.Println("Shutting down...")
|
|
}()
|
|
}
|
|
|
|
func getAniListAuthorizationToken(content string) AniListJWT {
|
|
apiUrl := "https://anilist.co/api/v2/oauth/token"
|
|
resource := "/api/v2/oauth/token"
|
|
data := url.Values{}
|
|
data.Set("grant_type", "authorization_code")
|
|
data.Set("client_id", Environment.ANILIST_APP_ID)
|
|
data.Set("client_secret", Environment.ANILIST_SECRET_TOKEN)
|
|
data.Set("redirect_uri", Environment.ANILIST_CALLBACK_URI)
|
|
data.Set("code", content)
|
|
|
|
u, _ := url.ParseRequestURI(apiUrl)
|
|
u.Path = resource
|
|
urlStr := u.String()
|
|
|
|
response, err := http.NewRequest("POST", urlStr, strings.NewReader(data.Encode()))
|
|
if err != nil {
|
|
log.Printf("Failed at response, %s\n", err)
|
|
}
|
|
response.Header.Add("Content-type", "application/x-www-form-urlencoded")
|
|
response.Header.Add("Accept", "application/json")
|
|
|
|
client := &http.Client{}
|
|
res, resErr := client.Do(response)
|
|
if resErr != nil {
|
|
log.Printf("Failed at res, %s\n", err)
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
returnedBody, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Printf("Could not read returned body, %s\n.", err)
|
|
}
|
|
|
|
var post AniListJWT
|
|
err = json.Unmarshal(returnedBody, &post)
|
|
if err != nil {
|
|
log.Printf("Failed at unmarshal, %s\n", err)
|
|
}
|
|
|
|
return post
|
|
}
|
|
|
|
func (a *App) GetAniListLoggedInUser() AniListUser {
|
|
a.AniListLogin()
|
|
body := struct {
|
|
Query string `json:"query"`
|
|
}{
|
|
Query: `
|
|
query {
|
|
Viewer {
|
|
id
|
|
name
|
|
avatar {
|
|
large
|
|
medium
|
|
}
|
|
bannerImage
|
|
siteUrl
|
|
}
|
|
}
|
|
`,
|
|
}
|
|
|
|
user, _ := AniListQuery(body, true)
|
|
|
|
var post AniListUser
|
|
err := json.Unmarshal(user, &post)
|
|
if err != nil {
|
|
log.Printf("Failed at unmarshal, %s\n", err)
|
|
}
|
|
|
|
return post
|
|
}
|
|
|
|
func (a *App) LogoutAniList() string {
|
|
if (AniListJWT{} != aniListJwt) {
|
|
if !aniRingReady() {
|
|
aniListJwt = AniListJWT{}
|
|
return "AniList Logged Out Successfully"
|
|
}
|
|
typeErr := aniRing.Remove("anilistTokenType")
|
|
expiresInErr := aniRing.Remove("anilistTokenExpiresIn")
|
|
accessTokenErr := aniRing.Remove("anilistAccessToken")
|
|
refreshTokenErr := aniRing.Remove("anilistRefreshToken")
|
|
if typeErr != nil || expiresInErr != nil || accessTokenErr != nil || refreshTokenErr != nil {
|
|
log.Printf("anilist: logout cleanup failed (type=%v expires=%v access=%v refresh=%v)", typeErr, expiresInErr, accessTokenErr, refreshTokenErr)
|
|
}
|
|
aniListJwt = AniListJWT{}
|
|
}
|
|
|
|
return "AniList Logged Out Successfully"
|
|
}
|