Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Frontend backend connection #8

Closed
wants to merge 6 commits into from
Closed
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
# Generated by Nix devshell
.devenv/
.direnv/

/backend/cmd/.env
Binary file added backend/cmd/cmd
Binary file not shown.
1 change: 1 addition & 0 deletions backend/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
func main() {
// Load environment variables
var config config.Config

if err := envconfig.Process(context.Background(), &config); err != nil {
log.Fatalln("Error processing .env file: ", err)
}
Expand Down
23 changes: 13 additions & 10 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,34 @@ go 1.22.6

require (
github.com/gofiber/fiber/v2 v2.52.5
github.com/jackc/pgx/v5 v5.6.0
github.com/jackc/pgx/v5 v5.7.1
github.com/lib/pq v1.10.9
)

require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/text v0.14.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/zmb3/spotify v1.3.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.18.0 // indirect
)

require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/goccy/go-json v0.10.3
github.com/google/uuid v1.5.0 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sethvargo/go-envconfig v1.1.0
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.17.0 // indirect
github.com/zmb3/spotify/v2 v2.4.2
golang.org/x/sys v0.25.0 // indirect
)
416 changes: 416 additions & 0 deletions backend/go.sum

Large diffs are not rendered by default.

12 changes: 7 additions & 5 deletions backend/internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package config

type Config struct {
DbHost string `env:"DB_HOST, required"` // the database host to connect to
DbPort string `env:"DB_PORT, required"` // the database port to connect to
DbUser string `env:"DB_USER, required"` // the user to connect to the database with
DbPassword string `env:"DB_PASSWORD, required"` // the password to connect to the database with
DbName string `env:"DB_NAME, required"` // the name of the database to connect to
DbHost string `env:"DB_HOST, required"` // the database host to connect to
DbPort string `env:"DB_PORT, required"` // the database port to connect to
DbUser string `env:"DB_USER, required"` // the user to connect to the database with
DbPassword string `env:"DB_PASSWORD, required"` // the password to connect to the database with
DbName string `env:"DB_NAME, required"` // the name of the database to connect to
SpClientKey string `env:"SP_CLIENT_KEY, required"` // the client key for the spotify api
SpClientSecret string `env:"SP_CLIENT_SECRET, required"` // the client secret for the spotify api

Port string `env:"PORT, default=8080"` // the port for the server to listen on
LogLevel string `env:"LOG_LEVEL, default=INFO"` // the level of event to log
Expand Down
161 changes: 161 additions & 0 deletions backend/internal/service/handler/spotify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package handler

import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"log"
"math/big"
"net/http"
"strings"

"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttpadaptor"
spotifyauth "github.com/zmb3/spotify/v2/auth"

"platnm/internal/config"

"github.com/gofiber/fiber/v2"
"github.com/sethvargo/go-envconfig"
"github.com/zmb3/spotify/v2"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
)

var (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why all this in global vars? would fields on a struct make more sense? the "constructor" of the struct could be responsible for err handling instead of having a global err

redirectURI = "http://localhost:8080/spotify/callback"
auth = spotifyauth.New(spotifyauth.WithRedirectURL(redirectURI), spotifyauth.WithScopes(spotifyauth.ScopeUserReadPrivate))
ch = make(chan *spotify.Client)
state = "abc123"
codeVerifier, err = generateRandomString(128)

Check failure on line 33 in backend/internal/service/handler/spotify.go

View workflow job for this annotation

GitHub Actions / backend-lint

var `err` is unused (unused)
)

func generateRandomString(length int) (string, error) {
const possible = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, length)
fmt.Printf("length: %v", length)

for i := range length {
fmt.Printf("i: %v", i)
index, err := rand.Int(rand.Reader, big.NewInt(int64(len(possible))))
if err != nil {
return "", err
}
result[i] = possible[index.Int64()]
}

return string(result), nil
}

func computeSHA256(plain string) (string, error) {
hasher := sha256.New()
_, err := hasher.Write([]byte(plain))
if err != nil {
return "", err
}
hash := hasher.Sum(nil)
return hex.EncodeToString(hash), nil
}

func base64Encode(input []byte) string {
str := base64.StdEncoding.EncodeToString(input)
str = strings.TrimRight(str, "=")
str = strings.ReplaceAll(str, "+", "-")
str = strings.ReplaceAll(str, "/", "_")
return str
}
func convertToHTTPRequest(ctx *fasthttp.RequestCtx) *http.Request {
req := new(http.Request)
err := fasthttpadaptor.ConvertRequest(ctx, req, true)
if err != nil {
return nil
}
return req
}

func CompleteAuth(c *fiber.Ctx) error {
tok, err := auth.Token(c.Context(), state, convertToHTTPRequest(c.Context()),
oauth2.SetAuthURLParam("code_verifier", codeVerifier))
if err != nil {
c.Status(fiber.StatusForbidden).SendString("Couldn't get token")

Check failure on line 83 in backend/internal/service/handler/spotify.go

View workflow job for this annotation

GitHub Actions / backend-lint

Error return value of `(*github.com/gofiber/fiber/v2.Ctx).SendString` is not checked (errcheck)
log.Fatal(err)
}

if st := c.FormValue("state"); st != state {
c.Status(fiber.StatusNotFound).SendString("State mismatch")

Check failure on line 88 in backend/internal/service/handler/spotify.go

View workflow job for this annotation

GitHub Actions / backend-lint

Error return value of `(*github.com/gofiber/fiber/v2.Ctx).SendString` is not checked (errcheck)
log.Fatalf("State mismatch: %s != %s\n", st, state)
}

// use the token to get an authenticated client
client := spotify.New(auth.Client(c.Context(), tok))
c.SendString("Login Completed!")

Check failure on line 94 in backend/internal/service/handler/spotify.go

View workflow job for this annotation

GitHub Actions / backend-lint

Error return value of `c.SendString` is not checked (errcheck)
ch <- client
return c.Status(fiber.StatusOK).JSON(ch)
}

func configurePKCE() spotify.Client {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be called on each req? or once on server init?

var config config.Config

if err := envconfig.Process(context.Background(), &config); err != nil {
log.Fatalln("Error processing .env file: ", err)
}

codeChallenge, err := computeSHA256(codeVerifier)
if err != nil {
log.Fatalf("couldn't compute SHA256: %v", err)
}

codeChallenge = base64Encode([]byte(codeChallenge))
url := auth.AuthURL(state,
oauth2.SetAuthURLParam("client_id", config.SpClientKey),
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
oauth2.SetAuthURLParam("code_challenge", codeChallenge),
)
fmt.Println("Please log in to Spotify by visiting the following page in your browser:", url)

// wait for auth to complete
client := <-ch
// /callback?code=AQDS_gNUxSbt5vMweZKrafGpw-xj1ZCEWvplmdzSpZoQTsshJ7dG1svBCsTw3KaKKfJGjXHm2-SzGhLCtv32YmqoswwHdHmXX5mqN5jNLV-FdWJ2ELG5T-9SUyLL2V9qnNyn6g9EllF2yBAeUAzfKvPX-NR-SLH_F2nIHl9cdCClOjvarT0BailMZMbC3FAgt71cbkRmMoXX04J5Hcx45afy_XFq7RqvehVC_AqIIYYYJm8MBh2apUjMiodz-k9QfFzefpA4CFFoIC5E2L91yCDFm9SuSQVLeXyNOT7Nd7qLfmBtYI1wnDs1dAJDtEVQiAPK&state=abc123

return *client
}

func configureClientCreds() spotify.Client {

Check failure on line 126 in backend/internal/service/handler/spotify.go

View workflow job for this annotation

GitHub Actions / backend-lint

func `configureClientCreds` is unused (unused)
var config config.Config

if err := envconfig.Process(context.Background(), &config); err != nil {
log.Fatalln("Error processing .env file: ", err)
}

ctx := context.Background()
authConfig := &clientcredentials.Config{
ClientID: config.SpClientKey,
ClientSecret: config.SpClientSecret,
TokenURL: spotifyauth.TokenURL,
}

accessToken, err := authConfig.Token(context.Background())
if err != nil {
log.Fatalf("couldn't get token: %v", err)
}

httpClient := spotifyauth.New().Client(ctx, accessToken)
client := *spotify.New(httpClient)
return client
}

func GetPlatnmPlaylist(c *fiber.Ctx) error {
ctx := context.Background()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ctx := context.Background()
ctx := c.Context()

try to refrain from context.Background(). from the stdlib docs:
"Background returns a non-nil, empty Context. It is never canceled, has no values, and has no deadline. It is typically used by the main function, initialization, and tests, and as the top-level Context for incoming requests."

client := configurePKCE()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if a context is used in configurePKCE(), you should drill down this req's ctx


playlistID := spotify.ID("671uu0Y7jiAgX04Ou82Up9?si=80a629645bb84d42")
playlist, err := client.GetPlaylist(ctx, playlistID)
if err != nil {
log.Fatalf("couldn't get playlist: %v", err)
}

return c.Status(fiber.StatusOK).JSON(playlist)
}
4 changes: 4 additions & 0 deletions backend/internal/service/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ func setupRoutes(app *fiber.App, conn *pgxpool.Pool) {
r.Get("/", userHandler.GetUsers)
r.Get("/:id", userHandler.GetUserById)
})
app.Route("/spotify", func(r fiber.Router) {
r.Get("/", handler.GetPlatnmPlaylist)
r.Get("/callback", handler.CompleteAuth)
})
}

func setupApp() *fiber.App {
Expand Down
47 changes: 34 additions & 13 deletions frontend/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,34 @@ import { HelloWave } from '@/components/HelloWave';
import ParallaxScrollView from '@/components/ParallaxScrollView';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { useEffect, useState } from 'react';
import axios from 'axios';

type User = {
email: string;
first_name: string;
last_name: string;
phone: string;
user_id: string;
};

export default function HomeScreen() {
const [isLoading, setLoading] = useState(true);
const [users, setUsers] = useState<User[]>([]);
const baseUrl = 'http://localhost:8080';

const getUsers = async () => {
axios.get(`${baseUrl}/users`).then((response) => {
setUsers(response.data);
console.log(response.data);
console.log(users);
});
};

useEffect(() => {
getUsers();
console.log(users);
}, []);
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
Expand All @@ -16,28 +42,23 @@ export default function HomeScreen() {
/>
}>
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">Welcome!</ThemedText>
<ThemedText type="title">Welcome to Platnm!</ThemedText>
<HelloWave />
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
<ThemedText>
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
Press{' '}
<ThemedText type="defaultSemiBold">
{Platform.select({ ios: 'cmd + d', android: 'cmd + m' })}
</ThemedText>{' '}
to open developer tools.
</ThemedText>
<ThemedText type="subtitle">Step 1: Backend Setup</ThemedText>
<ThemedText>
Make sure the database is connected and the backend server is running. Go to localhost:8080/users to see the list of users.
</ThemedText>
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 2: Explore</ThemedText>
<ThemedText type="subtitle">Step 2: Frontend Setup</ThemedText>
<ThemedText>
Tap the Explore tab to learn more about what's included in this starter app.
If frontend and backend are connected, you should see a phone number here : {users[0].phone}.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yayyy this is cool

</ThemedText>
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
<ThemedText type="subtitle">Step 3: Spotify Setup</ThemedText>
<ThemedText>
When you're ready, run{' '}
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
Expand Down
48 changes: 48 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading