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.
262 lines
6.7 KiB
Go
262 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"sync"
|
|
|
|
"github.com/99designs/keyring"
|
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
|
)
|
|
|
|
var simklJwt SimklJWT
|
|
|
|
var simklRing keyring.Keyring
|
|
|
|
func init() {
|
|
var err error
|
|
simklRing, err = keyring.Open(keyring.Config{
|
|
ServiceName: "AniTrack",
|
|
KeychainName: "AniTrack",
|
|
KeychainSynchronizable: false,
|
|
KeychainTrustApplication: true,
|
|
KeychainAccessibleWhenUnlocked: true,
|
|
})
|
|
if err != nil {
|
|
log.Printf("simkl: secure storage unavailable: %s", err)
|
|
}
|
|
}
|
|
|
|
func simklRingReady() bool {
|
|
if simklRing == nil {
|
|
log.Println("simkl: secure storage unavailable")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func simklRingSet(key string, data []byte) error {
|
|
if !simklRingReady() {
|
|
return errors.New("simkl: secure storage unavailable")
|
|
}
|
|
if err := simklRing.Set(keyring.Item{Key: key, Data: data}); err != nil {
|
|
log.Printf("simkl: save %s failed: %s", key, err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var simklCtxShutdown, simklCancel = context.WithCancel(context.Background())
|
|
|
|
func (a *App) CheckIfSimklLoggedIn() bool {
|
|
if (SimklJWT{} == simklJwt) {
|
|
if !simklRingReady() {
|
|
return false
|
|
}
|
|
tokenType, tokenTypeErr := simklRing.Get("SimklTokenType")
|
|
accessToken, accessTokenErr := simklRing.Get("SimklAccessToken")
|
|
scope, scopeErr := simklRing.Get("SimklScope")
|
|
if (tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil) || len(accessToken.Data) == 0 {
|
|
return false
|
|
} else {
|
|
simklJwt.TokenType = string(tokenType.Data)
|
|
simklJwt.AccessToken = string(accessToken.Data)
|
|
simklJwt.Scope = string(scope.Data)
|
|
return true
|
|
}
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
func (a *App) SimklLogin() {
|
|
if !a.CheckIfSimklLoggedIn() {
|
|
if !simklRingReady() {
|
|
log.Println("simkl: cannot check login, secure storage unavailable")
|
|
return
|
|
}
|
|
tokenType, tokenTypeErr := simklRing.Get("SimklTokenType")
|
|
accessToken, accessTokenErr := simklRing.Get("SimklAccessToken")
|
|
scope, scopeErr := simklRing.Get("SimklScope")
|
|
if (tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil) || len(accessToken.Data) == 0 {
|
|
getSimklCodeUrl := "https://simkl.com/oauth/authorize?response_type=code&client_id=" + Environment.SIMKL_CLIENT_ID + "&redirect_uri=" + Environment.SIMKL_CALLBACK_URI
|
|
runtime.BrowserOpenURL(*wailsContext, getSimklCodeUrl)
|
|
|
|
serverDone := &sync.WaitGroup{}
|
|
serverDone.Add(1)
|
|
a.handleSimklCallback(serverDone)
|
|
serverDone.Wait()
|
|
} else {
|
|
simklJwt.TokenType = string(tokenType.Data)
|
|
simklJwt.AccessToken = string(accessToken.Data)
|
|
simklJwt.Scope = string(scope.Data)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *App) handleSimklCallback(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 <-simklCtxShutdown.Done():
|
|
fmt.Println("Shutting down...")
|
|
return
|
|
default:
|
|
}
|
|
content := r.FormValue("code")
|
|
|
|
if content != "" {
|
|
simklJwt = getSimklAuthorizationToken(content)
|
|
_ = simklRingSet("SimklTokenType", []byte(simklJwt.TokenType))
|
|
_ = simklRingSet("SimklAccessToken", []byte(simklJwt.AccessToken))
|
|
_ = simklRingSet("SimklScope", []byte(simklJwt.Scope))
|
|
_, err := runtime.MessageDialog(*wailsContext, runtime.MessageDialogOptions{
|
|
Title: "Simkl Authorization",
|
|
Message: "It is now safe to close your browser tab",
|
|
})
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
fmt.Println("Shutting down...")
|
|
simklCancel()
|
|
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.Printf("Server error: %s\n", err)
|
|
}
|
|
fmt.Println("Shutting down...")
|
|
}()
|
|
}
|
|
|
|
func getSimklAuthorizationToken(content string) SimklJWT {
|
|
data := struct {
|
|
GrantType string `json:"grant_type"`
|
|
ClientID string `json:"client_id"`
|
|
ClientSecret string `json:"client_secret"`
|
|
RedirectURI string `json:"redirect_uri"`
|
|
Code string `json:"code"`
|
|
}{
|
|
GrantType: "authorization_code",
|
|
ClientID: Environment.SIMKL_CLIENT_ID,
|
|
ClientSecret: Environment.SIMKL_CLIENT_SECRET,
|
|
RedirectURI: Environment.SIMKL_CALLBACK_URI,
|
|
Code: content,
|
|
}
|
|
jsonData, err := json.Marshal(data)
|
|
if err != nil {
|
|
log.Printf("Failed to marshal data: %s\n", err)
|
|
return SimklJWT{}
|
|
}
|
|
|
|
response, err := http.NewRequest("POST", "https://api.simkl.com/oauth/token", bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
log.Printf("Failed at response, %s\n", err)
|
|
}
|
|
response.Header.Add("Content-Type", "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 SimklJWT
|
|
err = json.Unmarshal(returnedBody, &post)
|
|
if err != nil {
|
|
log.Printf("Failed at unmarshal, %s\n", err)
|
|
}
|
|
|
|
return post
|
|
}
|
|
|
|
func (a *App) GetSimklLoggedInUser() SimklUser {
|
|
a.SimklLogin()
|
|
|
|
client := &http.Client{}
|
|
|
|
req, _ := http.NewRequest("POST", "https://api.simkl.com/users/settings", nil)
|
|
|
|
req.Header.Add("Content-Type", "application/json")
|
|
req.Header.Add("Authorization", "Bearer "+simklJwt.AccessToken)
|
|
req.Header.Add("simkl-api-key", Environment.SIMKL_CLIENT_ID)
|
|
|
|
response, err := client.Do(req)
|
|
if err != nil {
|
|
log.Printf("Failed at request, %s\n", err)
|
|
return SimklUser{}
|
|
}
|
|
|
|
defer response.Body.Close()
|
|
|
|
respBody, _ := io.ReadAll(response.Body)
|
|
|
|
var errCheck struct {
|
|
Error string `json:"error"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
err = json.Unmarshal(respBody, &errCheck)
|
|
if err != nil {
|
|
log.Printf("Failed at unmarshal, %s\n", err)
|
|
}
|
|
|
|
if errCheck.Error != "" {
|
|
a.LogoutSimkl()
|
|
return SimklUser{}
|
|
}
|
|
|
|
var user SimklUser
|
|
|
|
err = json.Unmarshal(respBody, &user)
|
|
if err != nil {
|
|
log.Printf("Failed at unmarshal, %s\n", err)
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
func (a *App) LogoutSimkl() string {
|
|
if (SimklJWT{} != simklJwt) {
|
|
if !simklRingReady() {
|
|
simklJwt = SimklJWT{}
|
|
return "Simkl Logged Out Successfully"
|
|
}
|
|
tokenTypeErr := simklRing.Remove("SimklTokenType")
|
|
accessTokenErr := simklRing.Remove("SimklAccessToken")
|
|
scopeErr := simklRing.Remove("SimklScope")
|
|
|
|
if tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil {
|
|
log.Printf("simkl: logout cleanup failed (type=%v access=%v scope=%v)", tokenTypeErr, accessTokenErr, scopeErr)
|
|
}
|
|
simklJwt = SimklJWT{}
|
|
}
|
|
|
|
return "Simkl Logged Out Successfully"
|
|
}
|