Created
January 3, 2020 22:36
-
-
Save chew-z/86a44c1e0f3c63e15b4fb916bc91eeba to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"log" | |
"net/http" | |
"os" | |
"strings" | |
"time" | |
"github.com/gin-gonic/gin" | |
"github.com/patrickmn/go-cache" | |
"github.com/thinkerou/favicon" | |
"github.com/zmb3/spotify" | |
) | |
/* TODO | |
-- gracefull handling of zmb3/spotify errors | |
like 403 lack of scope | |
*/ | |
// const ( | |
// ) | |
var ( | |
kaszka = cache.New(60*time.Minute, 1*time.Minute) | |
auth = spotify.NewAuthenticator(redirectURI, spotify.ScopeUserReadPrivate, spotify.ScopeUserTopRead) | |
clientChannel = make(chan *spotify.Client) | |
redirectURI = os.Getenv("REDIRECT_URI") | |
) | |
func init() { | |
} | |
func main() { | |
router := gin.Default() | |
router.Use(favicon.New("./favicon.png")) | |
router.GET("/", func(c *gin.Context) { | |
c.String(http.StatusOK, "Hello World! This is auth-gin-cache.go here.") | |
}) | |
router.GET("/user", user) | |
router.GET("/top", top) | |
router.GET("/search", search) | |
router.GET("/callback", callback) | |
router.Run(":8080") | |
} | |
/* statefull authorization handler using channels | |
state = calling endpoint (which is intended use of scope) | |
caches client for as long as token is valid (1 hour for spotify) | |
no persistent storing of token, there is no need? | |
spotify stores persisten cookies behind our back so it is enough? | |
*/ | |
func callback(c *gin.Context) { | |
endpoint := c.Request.FormValue("state") | |
log.Printf("/callback: endpoint: %s", endpoint) | |
// Now we need different token for each endpoint = state. Sucks big way! | |
tok, err := auth.Token(endpoint, c.Request) | |
if err != nil { | |
c.String(http.StatusForbidden, "Couldn't get token") | |
log.Panic(err) | |
} | |
// create copy of gin.Context to be used inside the goroutine | |
// cCopy := c.Copy() | |
go func() { | |
client := auth.NewClient(tok) | |
log.Println("/callback: Login Completed!") | |
kaszka.Set(endpoint, &client, tok.Expiry.Sub(time.Now())) | |
log.Printf("/callback: Cached client for: %s", endpoint) | |
clientChannel <- &client | |
}() | |
url := fmt.Sprintf("http://%s%s?deuce=1", c.Request.Host, endpoint) | |
defer c.Redirect(303, url) | |
log.Printf("/callback: redirecting to endpoint %s", url) | |
} | |
/* | |
*/ | |
func user(c *gin.Context) { | |
endpoint := c.Request.URL.Path | |
client := getClient(endpoint) | |
if client == nil { // get client from oauth | |
if d := c.DefaultQuery("deuce", "0"); d == "1" { // wait for auth to complete | |
client = <-clientChannel | |
log.Println("/user: Login Completed!") | |
} else { // redirect to auth URL and exit | |
url := auth.AuthURL(endpoint) | |
log.Printf("%s: redirecting to %s", endpoint, url) | |
// HTTP standard does not pass through HTTP headers on an 302/301 directive | |
// 303 is never cached and always is GET | |
c.Redirect(303, url) | |
return | |
} | |
} | |
defer func() { | |
// use the client to make calls that require authorization | |
user, err := client.CurrentUser() | |
if err != nil { | |
log.Panic(err) | |
} | |
msg := fmt.Sprintf("You are logged in as: %s", user.ID) | |
c.String(http.StatusOK, msg) | |
}() | |
} | |
/* top - prints user top tracks (sensible defaults) | |
read zmb3/spotify code to learn more | |
*/ | |
func top(c *gin.Context) { | |
endpoint := c.Request.URL.Path | |
client := getClient(endpoint) | |
if client == nil { // get client from oauth | |
if d := c.DefaultQuery("deuce", "0"); d == "1" { // wait for auth to complete | |
client = <-clientChannel | |
log.Println("/user: Login Completed!") | |
} else { // redirect to auth URL and exit | |
url := auth.AuthURL(endpoint) | |
log.Printf("%s: redirecting to %s", endpoint, url) | |
// HTTP standard does not pass through HTTP headers on an 302/301 directive | |
// 303 is never cached and always is GET | |
c.Redirect(303, url) | |
return | |
} | |
} | |
defer func() { | |
// use the client to make calls that require authorization | |
top, err := client.CurrentUsersTopTracks() | |
if err != nil { | |
log.Panic(err) | |
c.String(http.StatusNotFound, err.Error()) | |
} | |
var b strings.Builder | |
b.WriteString("Top :") | |
for _, item := range top.Tracks { | |
b.WriteString("\n- ") | |
b.WriteString(item.Name) | |
b.WriteString(" [ ") | |
b.WriteString(item.Album.Name) | |
b.WriteString(" ] -- ") | |
b.WriteString(item.Artists[0].Name) | |
} | |
c.String(http.StatusOK, b.String()) | |
}() | |
} | |
/* search - searches playlists, albums, tracks etc. | |
*/ | |
func search(c *gin.Context) { | |
endpoint := c.Request.URL.Path | |
client := getClient(endpoint) | |
if client == nil { // get client from oauth | |
if d := c.DefaultQuery("deuce", "0"); d == "1" { // wait for auth to complete | |
client = <-clientChannel | |
log.Println("/search: Login Completed!") | |
// Edge case = WHAT TODO? | |
// - redirects erase search params | |
c.String(http.StatusOK, "Fix this edge case for /search") | |
} else { // redirect to auth URL and exit | |
url := auth.AuthURL(endpoint) | |
log.Printf("%s: redirecting to %s", endpoint, url) | |
// HTTP standard does not pass through HTTP headers on an 302/301 directive | |
// 303 is never cached and always is GET | |
c.Redirect(303, url) | |
return | |
} | |
} | |
defer func() { | |
query := c.DefaultQuery("q", "ABBA") | |
searchCategory := c.DefaultQuery("c", "track") | |
searchType := searchType(searchCategory) | |
results, err := client.Search(query, searchType) | |
if err != nil { | |
log.Println(err.Error()) | |
c.String(http.StatusNotFound, err.Error()) | |
return | |
} | |
resString := handleSearchResults(results) | |
c.String(http.StatusOK, resString) | |
}() | |
} | |
/* getClient - restore client for given state from cache | |
or return nil | |
*/ | |
func getClient(endpoint string) *spotify.Client { | |
if gclient, foundClient := kaszka.Get(endpoint); foundClient { | |
log.Printf("Cached client found for: %s", endpoint) | |
client := gclient.(*spotify.Client) | |
if tok, err := client.Token(); err != nil { | |
log.Panic(err) | |
} else { | |
log.Printf("Token will expire in %s", tok.Expiry.Sub(time.Now()).String()) | |
} | |
return client | |
} | |
msg := fmt.Sprintf("No cached client found for: %s", endpoint) | |
log.Println(msg) | |
return nil | |
} | |
func handleSearchResults(results *spotify.SearchResult) string { | |
var b strings.Builder | |
// handle album results | |
if results.Albums != nil { | |
b.WriteString("\nAlbums:\n") | |
for _, item := range results.Albums.Albums { | |
b.WriteString(fmt.Sprintf(" %s - %s : %s\n", item.ID, item.Name, item.Artists[0].Name)) | |
} | |
} | |
// handle playlist results | |
if results.Playlists != nil { | |
b.WriteString("\nPlaylists:\n") | |
for _, item := range results.Playlists.Playlists { | |
b.WriteString(fmt.Sprintf("- %s : %s\n", item.Name, item.Owner.DisplayName)) | |
} | |
} | |
// handle tracks results | |
if results.Tracks != nil { | |
b.WriteString("\nTracks:\n") | |
for _, item := range results.Tracks.Tracks { | |
b.WriteString(fmt.Sprintf(" %s - %s : %s\n", item.ID, item.Name, item.Album.Name)) | |
} | |
} | |
// handle artists results | |
if results.Artists != nil { | |
b.WriteString("\nArtists:\n") | |
for _, item := range results.Artists.Artists { | |
b.WriteString(fmt.Sprintf("- %s : %s\n", item.Name, item.Popularity)) | |
} | |
} | |
return b.String() | |
} | |
func searchType(a string) spotify.SearchType { | |
switch a { | |
case "track": | |
return spotify.SearchTypeTrack | |
case "playlist": | |
return spotify.SearchTypePlaylist | |
case "album": | |
return spotify.SearchTypeAlbum | |
case "artist": | |
return spotify.SearchTypeArtist | |
default: | |
return spotify.SearchTypeTrack | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"fmt" | |
"log" | |
"net/http" | |
"os" | |
"github.com/gin-gonic/gin" | |
"github.com/thinkerou/favicon" | |
"github.com/zmb3/spotify" | |
) | |
// const ( | |
// redirectURI = "http://localhost:8080/callback" | |
// ) | |
func init() { | |
} | |
/* Simplified code (no caching or no saving token) | |
for learning and experimenting with | |
Spotify (github.com/zmb3/spotify) oauth2 process | |
using goroutines and channels | |
TODO - state=/user returns state=abc123 | |
sometimes we are getting wrong state. I have up and think it is due to cookies | |
*/ | |
func main() { | |
clientChannel := make(chan *spotify.Client) | |
redirectURI := os.Getenv("REDIRECT_URI") | |
auth := spotify.NewAuthenticator(redirectURI, spotify.ScopeUserReadPrivate) | |
router := gin.Default() | |
router.Use(favicon.New("./favicon.png")) | |
router.GET("/", func(c *gin.Context) { | |
c.String(http.StatusOK, "Hello World!") | |
}) | |
router.GET("/abc123", func(c *gin.Context) { | |
if d := c.DefaultQuery("deuce", "0"); d == "1" { | |
client := <-clientChannel | |
{ | |
user, err := client.CurrentUser() | |
if err != nil { | |
log.Fatal(err) | |
} | |
msg := fmt.Sprintf("/abc123: You are logged in as: %s", user.ID) | |
c.String(http.StatusOK, msg) | |
} | |
} else { | |
c.String(http.StatusOK, "This page is for handling abc123 glitch") | |
} | |
}) | |
/* No redirect - manual copy/paste of authorization link */ | |
router.GET("/whoami", func(c *gin.Context) { | |
endpoint := c.Request.URL.Path | |
url := auth.AuthURL(endpoint) | |
log.Println("/whoami: Please log in to Spotify by visiting the following page in your browser:", url) | |
// wait for auth to complete | |
client := <-clientChannel | |
{ | |
// use the client to make calls that require authorization | |
user, err := client.CurrentUser() | |
if err != nil { | |
log.Fatal(err) | |
} | |
msg := fmt.Sprintf("You are logged in as: %s", user.ID) | |
log.Println("/whoami: Login Completed!") | |
c.String(http.StatusOK, msg) | |
} | |
}) | |
/* */ | |
router.GET("/user", func(c *gin.Context) { | |
endpoint := c.Request.URL.Path | |
url := auth.AuthURL(endpoint) | |
if d := c.DefaultQuery("deuce", "0"); d == "1" { | |
// wait for auth to complete | |
client := <-clientChannel | |
{ | |
// use the client to make calls that require authorization | |
user, err := client.CurrentUser() | |
if err != nil { | |
log.Fatal(err) | |
} | |
msg := fmt.Sprintf("You are logged in as: %s", user.ID) | |
log.Println("/random: Login Completed!") | |
c.String(http.StatusOK, msg) | |
} | |
} else { | |
// log.Println("/auth: Please log in to Spotify by visiting the following page in your browser:", url) | |
// HTTP standard does not pass through HTTP headers on an 302/301 directive | |
// 303 is never cached and always is GET | |
c.Redirect(303, url) | |
} | |
}) | |
/* simple statefull authorization handler using channels | |
it is expected that parameter state = (FormValue("state") | |
contains proper endpoint where we should redirect | |
This is not always the case due to browser/spotify cookies/ | |
imperfect implementattion of oauth2 in go etc. | |
*/ | |
router.GET("/callback", func(c *gin.Context) { | |
endpoint := c.Request.FormValue("state") | |
log.Printf("/callback: endpoint: %s", endpoint) | |
tok, err := auth.Token(endpoint, c.Request) | |
if err != nil { | |
c.String(http.StatusForbidden, "Couldn't get token") | |
log.Fatal(err) | |
} | |
// create copy of gin.Context to be used inside the goroutine | |
// cCopy := c.Copy() | |
go func() { | |
client := auth.NewClient(tok) | |
log.Println("/callback: Login Completed!") | |
clientChannel <- &client | |
}() | |
url := fmt.Sprintf("http://%s%s?deuce=1", c.Request.Host, endpoint) | |
defer c.Redirect(303, url) | |
log.Printf("callback: redirecting to endpoint %s", url) | |
}) | |
router.Run(":8080") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment