Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
FROM node:12.18.2 as build

FROM oven/bun:1.3.9 AS build



WORKDIR /app

COPY ./package.json /app/package.json
COPY ./package-lock.json /app/package-lock.json
COPY package.json ./
COPY bun.lock ./

RUN npm install
RUN bun install
COPY . .
RUN bun run build

RUN npm run build

FROM golang:latest
FROM golang:tip-alpine3.22

WORKDIR /go/src/app
COPY --from=build /app/public public
COPY server/go.mod .
COPY server/go.sum .
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY server .
COPY . .


EXPOSE 5000
RUN go build -o rest-server main.go
RUN go build -o rest-server cmd/server/main.go
CMD ["./rest-server"]
366 changes: 366 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

164 changes: 164 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package main

import (
"encoding/base64"
"encoding/json"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"

_ "github.com/joho/godotenv/autoload"
)

type body struct {
State string `json:"state"`
}

func authorize(w http.ResponseWriter, req *http.Request) {

isDev := os.Getenv("DEVELOPMENT") == "1"
log.Println(isDev)

if req.Method != http.MethodPost {
w.WriteHeader(http.StatusNotFound)
return
}

w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
if isDev {
w.Header().Set("Access-Control-Allow-Origin", "*")
}

baseUrl := os.Getenv("BASE_URL")
clientId := os.Getenv("CLIENT_ID")
scopes := os.Getenv("SCOPES")
redirectUri := os.Getenv("REDIRECT_URL")
responseType := os.Getenv("RESPONSE_TYPE")

var b body
err := json.NewDecoder(req.Body).Decode(&b)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Println(b)
authUrl := baseUrl + "/authorize" + "?client_id=" + clientId + "&redirect_uri=" + redirectUri + "&scope=" + scopes + "&response_type=" + responseType + "&state=" + b.State
log.Println((authUrl))
w.Write([]byte(authUrl))
}

func callback(w http.ResponseWriter, req *http.Request) {
queryParams := req.URL.Query()
state := queryParams.Get("state")
log.Println("state:", state)
code := queryParams.Get("code")
log.Println("code:", code)

clientId := os.Getenv("CLIENT_ID")
clientSecret := os.Getenv("CLIENT_SECRET")
redirectUri := os.Getenv("REDIRECT_URL")
isDev := os.Getenv("DEVELOPMENT") == "1"

tokenResp, err := requestToken(code, redirectUri, clientId, clientSecret)
if err != nil {
log.Println(err)
http.Error(w, "token request failed", http.StatusInternalServerError)
return
}
log.Println(tokenResp)
accessToken, _ := tokenResp["access_token"].(string)
refreshToken, _ := tokenResp["refresh_token"].(string)
scope, _ := tokenResp["scope"].(string)
var expiresIn int
if v, ok := tokenResp["expires_in"].(float64); ok {
expiresIn = int(v)
} else {
expiresIn = 3600
}
accessCookie := &http.Cookie{
Name: "access_token",
Value: accessToken,
Path: "/",
MaxAge: expiresIn,
Secure: !isDev,
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(w, accessCookie)
scopeCookie := &http.Cookie{
Name: "scope",
Value: scope,
Path: "/",
MaxAge: expiresIn,
Secure: !isDev,
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(w, scopeCookie)
if refreshToken != "" {
refreshCookie := &http.Cookie{
Name: "refresh_token",
Value: refreshToken,
Path: "/",
// keep refresh longer (e.g. 30 days) or use an env override
MaxAge: 30 * 24 * 60 * 60,
Secure: !isDev,
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(w, refreshCookie)
}

http.Redirect(w, req, "/", http.StatusFound)

}

// requestToken exchanges an authorization code for tokens from Spotify.
func requestToken(code, redirectURI, clientID, clientSecret string) (map[string]interface{}, error) {
data := url.Values{}
data.Set("code", code)
data.Set("redirect_uri", redirectURI)
data.Set("grant_type", "authorization_code")

req, err := http.NewRequest(http.MethodPost, "https://accounts.spotify.com/api/token", strings.NewReader(data.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
auth := base64.StdEncoding.EncodeToString([]byte(clientID + ":" + clientSecret))
req.Header.Set("Authorization", "Basic "+auth)

client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
return result, nil
}

func main() {

http.Handle("/", http.FileServer(http.Dir("./public")))
http.HandleFunc("/authorize", authorize)
http.HandleFunc("/callback", callback)
// http.HandleFunc("/login", login)

port := os.Getenv("PORT")
log.Println("Starting server on PORT:" + port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
File renamed without changes.
File renamed without changes.
72 changes: 0 additions & 72 deletions server/.gitignore

This file was deleted.

57 changes: 0 additions & 57 deletions server/main.go

This file was deleted.