Consolidate frontend REST calls with GraphQL
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 1s
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 1s
Replace 5 separate REST calls on home page load with a single GraphQL query. Add homeData store that fetches posts, favorites, activities, spotify, and auth in one request. Convert all admin mutations and auth flows to use GraphQL. Add album images to Spotify GraphQL schema. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
456
backend/graph/schema.resolvers.go
Normal file
456
backend/graph/schema.resolvers.go
Normal file
@@ -0,0 +1,456 @@
|
||||
package graph
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver
|
||||
// implementations
|
||||
// will be copied through when generating and any unknown code will be moved to the end.
|
||||
// Code generated by github.com/99designs/gqlgen version v0.17.88
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"adam-french.co.uk/backend/graph/model"
|
||||
"adam-french.co.uk/backend/models"
|
||||
spotify "github.com/zmb3/spotify/v2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Login is the resolver for the login field.
|
||||
func (r *mutationResolver) Login(ctx context.Context, input model.LoginInput) (*model.AuthPayload, error) {
|
||||
gc := GinContextFromCtx(ctx)
|
||||
if gc == nil {
|
||||
return nil, fmt.Errorf("could not get gin context")
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := r.Store.DB.Where("username = ?", input.Username).First(&user).Error; err != nil {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword(user.Password, []byte(input.Password)); err != nil {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
|
||||
tokens, err := r.Store.Auth.GenerateJWT(&user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate tokens")
|
||||
}
|
||||
|
||||
gc.SetSameSite(http.SameSiteLaxMode)
|
||||
gc.SetCookie(
|
||||
"access_token",
|
||||
tokens.AccessToken,
|
||||
int(r.Store.Auth.Config.AccessTokenLifetime.Seconds()),
|
||||
r.Store.Auth.Config.Endpoint,
|
||||
r.Store.Auth.Config.Domain,
|
||||
true, true,
|
||||
)
|
||||
gc.SetCookie(
|
||||
"refresh_token",
|
||||
tokens.RefreshToken,
|
||||
int(r.Store.Auth.Config.RefreshTokenLifetime.Seconds()),
|
||||
r.Store.Auth.Config.Endpoint,
|
||||
r.Store.Auth.Config.Domain,
|
||||
true, true,
|
||||
)
|
||||
|
||||
return &model.AuthPayload{User: &user}, nil
|
||||
}
|
||||
|
||||
// Logout is the resolver for the logout field.
|
||||
func (r *mutationResolver) Logout(ctx context.Context) (bool, error) {
|
||||
gc := GinContextFromCtx(ctx)
|
||||
if gc == nil {
|
||||
return false, fmt.Errorf("could not get gin context")
|
||||
}
|
||||
|
||||
gc.SetSameSite(http.SameSiteLaxMode)
|
||||
gc.SetCookie("access_token", "", -1, "", "", true, true)
|
||||
gc.SetCookie("refresh_token", "", -1, "", "", true, true)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RefreshToken is the resolver for the refreshToken field.
|
||||
func (r *mutationResolver) RefreshToken(ctx context.Context) (*model.AuthPayload, error) {
|
||||
gc := GinContextFromCtx(ctx)
|
||||
if gc == nil {
|
||||
return nil, fmt.Errorf("could not get gin context")
|
||||
}
|
||||
|
||||
refreshToken, err := gc.Cookie("refresh_token")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
claims, err := r.Store.Auth.VerifyJWT(refreshToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
userIDF, ok := (*claims)["id"].(float64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid token claims")
|
||||
}
|
||||
|
||||
var user models.User
|
||||
user.ID = uint(userIDF)
|
||||
if err := r.Store.DB.First(&user).Error; err != nil {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
tokens, err := r.Store.Auth.GenerateJWT(&user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate tokens")
|
||||
}
|
||||
|
||||
gc.SetSameSite(http.SameSiteLaxMode)
|
||||
gc.SetCookie(
|
||||
"access_token",
|
||||
tokens.AccessToken,
|
||||
int(r.Store.Auth.Config.AccessTokenLifetime.Seconds()),
|
||||
r.Store.Auth.Config.Endpoint,
|
||||
r.Store.Auth.Config.Domain,
|
||||
true, true,
|
||||
)
|
||||
gc.SetCookie(
|
||||
"refresh_token",
|
||||
tokens.RefreshToken,
|
||||
int(r.Store.Auth.Config.RefreshTokenLifetime.Seconds()),
|
||||
r.Store.Auth.Config.Endpoint,
|
||||
r.Store.Auth.Config.Domain,
|
||||
true, true,
|
||||
)
|
||||
|
||||
return &model.AuthPayload{User: &user}, nil
|
||||
}
|
||||
|
||||
// CreatePost is the resolver for the createPost field.
|
||||
func (r *mutationResolver) CreatePost(ctx context.Context, input model.CreatePostInput) (*models.Post, error) {
|
||||
if !IsAdminFromCtx(ctx) {
|
||||
return nil, fmt.Errorf("admin access required")
|
||||
}
|
||||
|
||||
userID, ok := UserIDFromCtx(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
post := models.Post{Title: input.Title, Content: input.Content, AuthorID: userID}
|
||||
if err := r.Store.DB.Create(&post).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
// UpdatePost is the resolver for the updatePost field.
|
||||
func (r *mutationResolver) UpdatePost(ctx context.Context, id int, input model.UpdatePostInput) (*models.Post, error) {
|
||||
if !IsAdminFromCtx(ctx) {
|
||||
return nil, fmt.Errorf("admin access required")
|
||||
}
|
||||
|
||||
userID, ok := UserIDFromCtx(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
var post models.Post
|
||||
if err := r.Store.DB.First(&post, id).Error; err != nil {
|
||||
return nil, fmt.Errorf("post not found")
|
||||
}
|
||||
|
||||
if post.AuthorID != userID {
|
||||
return nil, fmt.Errorf("you can only update your own posts")
|
||||
}
|
||||
|
||||
post.Title = input.Title
|
||||
post.Content = input.Content
|
||||
if err := r.Store.DB.Save(&post).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
// DeletePost is the resolver for the deletePost field.
|
||||
func (r *mutationResolver) DeletePost(ctx context.Context, id int) (*models.Post, error) {
|
||||
if !IsAdminFromCtx(ctx) {
|
||||
return nil, fmt.Errorf("admin access required")
|
||||
}
|
||||
|
||||
userID, ok := UserIDFromCtx(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
var post models.Post
|
||||
if err := r.Store.DB.First(&post, id).Error; err != nil {
|
||||
return nil, fmt.Errorf("post not found")
|
||||
}
|
||||
|
||||
if post.AuthorID != userID {
|
||||
return nil, fmt.Errorf("you can only delete your own posts")
|
||||
}
|
||||
|
||||
r.Store.DB.Delete(&post)
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
// CreateUser is the resolver for the createUser field.
|
||||
func (r *mutationResolver) CreateUser(ctx context.Context, input model.CreateUserInput) (*models.User, error) {
|
||||
if !IsAdminFromCtx(ctx) {
|
||||
return nil, fmt.Errorf("admin access required")
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := models.User{Username: input.Username, Password: hashedPassword}
|
||||
if err := r.Store.DB.Create(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// DeleteUser is the resolver for the deleteUser field.
|
||||
func (r *mutationResolver) DeleteUser(ctx context.Context, id int) (*models.User, error) {
|
||||
if !IsAdminFromCtx(ctx) {
|
||||
return nil, fmt.Errorf("admin access required")
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := r.Store.DB.First(&user, id).Error; err != nil {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
if err := r.Store.DB.Delete(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// SetUserAdmin is the resolver for the setUserAdmin field.
|
||||
func (r *mutationResolver) SetUserAdmin(ctx context.Context, id int, admin bool) (*models.User, error) {
|
||||
if !IsAdminFromCtx(ctx) {
|
||||
return nil, fmt.Errorf("admin access required")
|
||||
}
|
||||
|
||||
callerID, ok := UserIDFromCtx(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
if uint(id) == callerID {
|
||||
return nil, fmt.Errorf("cannot change your own admin status")
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := r.Store.DB.First(&user, id).Error; err != nil {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
user.Admin = admin
|
||||
if err := r.Store.DB.Save(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// CreateFavorite is the resolver for the createFavorite field.
|
||||
func (r *mutationResolver) CreateFavorite(ctx context.Context, input model.CreateFavoriteInput) (*models.Favorite, error) {
|
||||
if !IsAdminFromCtx(ctx) {
|
||||
return nil, fmt.Errorf("admin access required")
|
||||
}
|
||||
|
||||
favorite := models.Favorite{Type: input.Type, Name: input.Name, Link: input.Link}
|
||||
if err := r.Store.DB.Create(&favorite).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &favorite, nil
|
||||
}
|
||||
|
||||
// CreateActivity is the resolver for the createActivity field.
|
||||
func (r *mutationResolver) CreateActivity(ctx context.Context, input model.CreateActivityInput) (*models.Activity, error) {
|
||||
if !IsAdminFromCtx(ctx) {
|
||||
return nil, fmt.Errorf("admin access required")
|
||||
}
|
||||
|
||||
activity := models.Activity{Type: input.Type, Name: input.Name, Link: input.Link}
|
||||
if err := r.Store.DB.Create(&activity).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &activity, nil
|
||||
}
|
||||
|
||||
// Users is the resolver for the users field.
|
||||
func (r *queryResolver) Users(ctx context.Context) ([]*models.User, error) {
|
||||
var users []models.User
|
||||
if err := r.Store.DB.Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*models.User, len(users))
|
||||
for i := range users {
|
||||
result[i] = &users[i]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// User is the resolver for the user field.
|
||||
func (r *queryResolver) User(ctx context.Context, id int) (*models.User, error) {
|
||||
var user models.User
|
||||
if err := r.Store.DB.First(&user, id).Error; err != nil {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Posts is the resolver for the posts field.
|
||||
func (r *queryResolver) Posts(ctx context.Context) ([]*models.Post, error) {
|
||||
var posts []models.Post
|
||||
if err := r.Store.DB.Preload("Author").Order("created_at DESC").Find(&posts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*models.Post, len(posts))
|
||||
for i := range posts {
|
||||
result[i] = &posts[i]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Post is the resolver for the post field.
|
||||
func (r *queryResolver) Post(ctx context.Context, id int) (*models.Post, error) {
|
||||
var post models.Post
|
||||
if err := r.Store.DB.Preload("Author").First(&post, id).Error; err != nil {
|
||||
return nil, fmt.Errorf("post not found")
|
||||
}
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
// Activities is the resolver for the activities field.
|
||||
func (r *queryResolver) Activities(ctx context.Context) ([]*models.Activity, error) {
|
||||
var activities []models.Activity
|
||||
if err := r.Store.DB.Order("created_at DESC").Find(&activities).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*models.Activity, len(activities))
|
||||
for i := range activities {
|
||||
result[i] = &activities[i]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Favorites is the resolver for the favorites field.
|
||||
func (r *queryResolver) Favorites(ctx context.Context) ([]*models.Favorite, error) {
|
||||
var favorites []models.Favorite
|
||||
if err := r.Store.DB.Order("created_at DESC").Find(&favorites).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*models.Favorite, len(favorites))
|
||||
for i := range favorites {
|
||||
result[i] = &favorites[i]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RowingSessions is the resolver for the rowingSessions field.
|
||||
func (r *queryResolver) RowingSessions(ctx context.Context) ([]*models.Rowing, error) {
|
||||
var rows []models.Rowing
|
||||
if err := r.Store.DB.Order("created_at DESC").Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*models.Rowing, len(rows))
|
||||
for i := range rows {
|
||||
result[i] = &rows[i]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Messages is the resolver for the messages field.
|
||||
func (r *queryResolver) Messages(ctx context.Context) ([]*models.Message, error) {
|
||||
var messages []models.Message
|
||||
if err := r.Store.DB.Order("created_at DESC").Find(&messages).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]*models.Message, len(messages))
|
||||
for i := range messages {
|
||||
result[i] = &messages[i]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SpotifyListening is the resolver for the spotifyListening field.
|
||||
func (r *queryResolver) SpotifyListening(ctx context.Context) (*model.SpotifyPlaying, error) {
|
||||
if r.Store.SpotifyClient == nil {
|
||||
return nil, fmt.Errorf("Spotify not authenticated")
|
||||
}
|
||||
|
||||
playing, err := r.Store.SpotifyClient.PlayerCurrentlyPlaying(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &model.SpotifyPlaying{Playing: playing.Playing}
|
||||
if playing.Item != nil {
|
||||
result.Track = mapSpotifyTrack(playing.Item)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SpotifyRecent is the resolver for the spotifyRecent field.
|
||||
func (r *queryResolver) SpotifyRecent(ctx context.Context) ([]*model.SpotifyRecentItem, error) {
|
||||
if r.Store.SpotifyClient == nil {
|
||||
return nil, fmt.Errorf("Spotify not authenticated")
|
||||
}
|
||||
|
||||
if r.Store.RecentSongsFresh() {
|
||||
return mapRecentItems(*r.Store.RecentSongs), nil
|
||||
}
|
||||
|
||||
opts := spotify.RecentlyPlayedOptions{Limit: 3}
|
||||
played, err := r.Store.SpotifyClient.PlayerRecentlyPlayedOpt(ctx, &opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.Store.RecentSongs = &played
|
||||
r.Store.RecentSongsFetchedAt = time.Now()
|
||||
|
||||
return mapRecentItems(played), nil
|
||||
}
|
||||
|
||||
// Me is the resolver for the me field.
|
||||
func (r *queryResolver) Me(ctx context.Context) (*models.User, error) {
|
||||
userID, ok := UserIDFromCtx(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
var user models.User
|
||||
user.ID = userID
|
||||
if err := r.Store.DB.First(&user).Error; err != nil {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Mutation returns MutationResolver implementation.
|
||||
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
|
||||
|
||||
// Query returns QueryResolver implementation.
|
||||
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
|
||||
|
||||
type mutationResolver struct{ *Resolver }
|
||||
type queryResolver struct{ *Resolver }
|
||||
|
||||
Reference in New Issue
Block a user