Split schema.graphql and schema.resolvers.go into per-domain files
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m36s

Move Query/Mutation field declarations from the monolithic schema.graphql
into each domain's .graphql file using extend type, so gqlgen places
resolvers in the matching *.resolvers.go files. Extract helper functions
into *_helpers.go files to prevent gqlgen from commenting them out.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 18:57:23 +01:00
parent 37171478b1
commit b96b7d7a93
32 changed files with 2184 additions and 2071 deletions

View File

@@ -7,7 +7,9 @@ package graph
import ( import (
"context" "context"
"fmt"
"adam-french.co.uk/backend/graph/model"
"adam-french.co.uk/backend/models" "adam-french.co.uk/backend/models"
) )
@@ -16,6 +18,33 @@ func (r *activityResolver) ID(ctx context.Context, obj *models.Activity) (int, e
return int(obj.ID), nil return int(obj.ID), 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
}
// 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
}
// Activity returns ActivityResolver implementation. // Activity returns ActivityResolver implementation.
func (r *Resolver) Activity() ActivityResolver { return &activityResolver{r} } func (r *Resolver) Activity() ActivityResolver { return &activityResolver{r} }

View File

@@ -0,0 +1,146 @@
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"
"adam-french.co.uk/backend/graph/model"
"adam-french.co.uk/backend/models"
"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")
}
if !r.Store.LoginLimiter.Allow(gc.ClientIP()) {
return nil, fmt.Errorf("too many login attempts, please try again later")
}
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.Domain,
true, true,
)
gc.SetCookie(
"refresh_token",
tokens.RefreshToken,
int(r.Store.Auth.Config.RefreshTokenLifetime.Seconds()),
"/",
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, "/", r.Store.Auth.Config.Domain, true, true)
gc.SetCookie("refresh_token", "", -1, "/", r.Store.Auth.Config.Domain, 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.Domain,
true, true,
)
gc.SetCookie(
"refresh_token",
tokens.RefreshToken,
int(r.Store.Auth.Config.RefreshTokenLifetime.Seconds()),
"/",
r.Store.Auth.Config.Domain,
true, true,
)
return &model.AuthPayload{User: &user}, 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
}

View File

@@ -7,7 +7,9 @@ package graph
import ( import (
"context" "context"
"fmt"
"adam-french.co.uk/backend/graph/model"
"adam-french.co.uk/backend/models" "adam-french.co.uk/backend/models"
) )
@@ -16,6 +18,46 @@ func (r *bookmarkResolver) ID(ctx context.Context, obj *models.Bookmark) (int, e
return int(obj.ID), nil return int(obj.ID), nil
} }
// CreateBookmark is the resolver for the createBookmark field.
func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.CreateBookmarkInput) (*models.Bookmark, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
bookmark := models.Bookmark{Category: input.Category, Name: input.Name, Link: input.Link}
if err := r.Store.DB.Create(&bookmark).Error; err != nil {
return nil, err
}
return &bookmark, nil
}
// DeleteBookmark is the resolver for the deleteBookmark field.
func (r *mutationResolver) DeleteBookmark(ctx context.Context, id int) (*models.Bookmark, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
var bookmark models.Bookmark
if err := r.Store.DB.First(&bookmark, id).Error; err != nil {
return nil, err
}
if err := r.Store.DB.Delete(&bookmark).Error; err != nil {
return nil, err
}
return &bookmark, nil
}
// Bookmarks is the resolver for the bookmarks field.
func (r *queryResolver) Bookmarks(ctx context.Context) ([]*models.Bookmark, error) {
var bookmarks []models.Bookmark
if err := r.Store.DB.Order("category ASC, created_at ASC").Find(&bookmarks).Error; err != nil {
return nil, err
}
result := make([]*models.Bookmark, len(bookmarks))
for i := range bookmarks {
result[i] = &bookmarks[i]
}
return result, nil
}
// Bookmark returns BookmarkResolver implementation. // Bookmark returns BookmarkResolver implementation.
func (r *Resolver) Bookmark() BookmarkResolver { return &bookmarkResolver{r} } func (r *Resolver) Bookmark() BookmarkResolver { return &bookmarkResolver{r} }

View File

@@ -7,7 +7,9 @@ package graph
import ( import (
"context" "context"
"fmt"
"adam-french.co.uk/backend/graph/model"
"adam-french.co.uk/backend/models" "adam-french.co.uk/backend/models"
) )
@@ -16,6 +18,33 @@ func (r *favoriteResolver) ID(ctx context.Context, obj *models.Favorite) (int, e
return int(obj.ID), nil return int(obj.ID), 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
}
// 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
}
// Favorite returns FavoriteResolver implementation. // Favorite returns FavoriteResolver implementation.
func (r *Resolver) Favorite() FavoriteResolver { return &favoriteResolver{r} } func (r *Resolver) Favorite() FavoriteResolver { return &favoriteResolver{r} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,10 @@
package graph 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 ( import (
"context" "context"
"time" "time"
@@ -8,17 +13,6 @@ import (
"adam-french.co.uk/backend/services" "adam-french.co.uk/backend/services"
) )
func mapGiteaFeed(feed *services.GiteaFeedResponse) *model.GiteaFeedItem {
return &model.GiteaFeedItem{
AvatarURL: feed.ActUser.AvatarURL,
RepoURL: feed.Repo.HTMLURL,
RepoName: feed.Repo.FullName,
OpType: feed.OpType,
CommitMessage: services.ParseCommitMessage(feed.Content),
CreatedAt: feed.Created,
}
}
// GiteaFeed is the resolver for the giteaFeed field. // GiteaFeed is the resolver for the giteaFeed field.
func (r *queryResolver) GiteaFeed(ctx context.Context) (*model.GiteaFeedItem, error) { func (r *queryResolver) GiteaFeed(ctx context.Context) (*model.GiteaFeedItem, error) {
if r.Store.GiteaFeedFresh() { if r.Store.GiteaFeedFresh() {

View File

@@ -0,0 +1,17 @@
package graph
import (
"adam-french.co.uk/backend/graph/model"
"adam-french.co.uk/backend/services"
)
func mapGiteaFeed(feed *services.GiteaFeedResponse) *model.GiteaFeedItem {
return &model.GiteaFeedItem{
AvatarURL: feed.ActUser.AvatarURL,
RepoURL: feed.Repo.HTMLURL,
RepoName: feed.Repo.FullName,
OpType: feed.OpType,
CommitMessage: services.ParseCommitMessage(feed.Content),
CreatedAt: feed.Created,
}
}

View File

@@ -7,7 +7,9 @@ package graph
import ( import (
"context" "context"
"fmt"
"adam-french.co.uk/backend/graph/model"
"adam-french.co.uk/backend/models" "adam-french.co.uk/backend/models"
) )
@@ -16,6 +18,75 @@ func (r *jobAppReferenceResolver) ID(ctx context.Context, obj *models.JobAppRefe
return int(obj.ID), nil return int(obj.ID), nil
} }
// CreateJobAppReference is the resolver for the createJobAppReference field.
func (r *mutationResolver) CreateJobAppReference(ctx context.Context, input model.CreateJobAppReferenceInput) (*models.JobAppReference, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
ref := models.JobAppReference{
Category: input.Category,
Label: input.Label,
Value: input.Value,
}
if input.SortOrder != nil {
ref.SortOrder = *input.SortOrder
}
if err := r.Store.DB.Create(&ref).Error; err != nil {
return nil, err
}
return &ref, nil
}
// UpdateJobAppReference is the resolver for the updateJobAppReference field.
func (r *mutationResolver) UpdateJobAppReference(ctx context.Context, id int, input model.UpdateJobAppReferenceInput) (*models.JobAppReference, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
var ref models.JobAppReference
if err := r.Store.DB.First(&ref, id).Error; err != nil {
return nil, err
}
if input.Category != nil {
ref.Category = *input.Category
}
if input.Label != nil {
ref.Label = *input.Label
}
if input.Value != nil {
ref.Value = *input.Value
}
if input.SortOrder != nil {
ref.SortOrder = *input.SortOrder
}
if err := r.Store.DB.Save(&ref).Error; err != nil {
return nil, err
}
return &ref, nil
}
// DeleteJobAppReference is the resolver for the deleteJobAppReference field.
func (r *mutationResolver) DeleteJobAppReference(ctx context.Context, id int) (bool, error) {
if !IsAdminFromCtx(ctx) {
return false, fmt.Errorf("admin access required")
}
if err := r.Store.DB.Delete(&models.JobAppReference{}, id).Error; err != nil {
return false, err
}
return true, nil
}
// JobAppReferences is the resolver for the jobAppReferences field.
func (r *queryResolver) JobAppReferences(ctx context.Context) ([]*models.JobAppReference, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
var refs []*models.JobAppReference
if err := r.Store.DB.Order("category ASC, sort_order ASC, created_at ASC").Find(&refs).Error; err != nil {
return nil, err
}
return refs, nil
}
// JobAppReference returns JobAppReferenceResolver implementation. // JobAppReference returns JobAppReferenceResolver implementation.
func (r *Resolver) JobAppReference() JobAppReferenceResolver { return &jobAppReferenceResolver{r} } func (r *Resolver) JobAppReference() JobAppReferenceResolver { return &jobAppReferenceResolver{r} }

View File

@@ -7,7 +7,9 @@ package graph
import ( import (
"context" "context"
"fmt"
"adam-french.co.uk/backend/graph/model"
"adam-french.co.uk/backend/models" "adam-french.co.uk/backend/models"
) )
@@ -16,6 +18,97 @@ func (r *jobApplicationResolver) ID(ctx context.Context, obj *models.JobApplicat
return int(obj.ID), nil return int(obj.ID), nil
} }
// CreateJobApplication is the resolver for the createJobApplication field.
func (r *mutationResolver) CreateJobApplication(ctx context.Context, input model.CreateJobApplicationInput) (*models.JobApplication, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
app := models.JobApplication{
JobTitle: input.JobTitle,
Company: input.Company,
Location: input.Location,
URL: input.URL,
Status: input.Status,
Notes: input.Notes,
AppliedAt: input.AppliedAt,
}
if err := r.Store.DB.Create(&app).Error; err != nil {
return nil, err
}
return &app, nil
}
// UpdateJobApplication is the resolver for the updateJobApplication field.
func (r *mutationResolver) UpdateJobApplication(ctx context.Context, id int, input model.UpdateJobApplicationInput) (*models.JobApplication, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
var app models.JobApplication
if err := r.Store.DB.First(&app, id).Error; err != nil {
return nil, err
}
if input.JobTitle != nil {
app.JobTitle = *input.JobTitle
}
if input.Company != nil {
app.Company = *input.Company
}
if input.Location != nil {
app.Location = input.Location
}
if input.URL != nil {
app.URL = input.URL
}
if input.Status != nil {
app.Status = *input.Status
}
if input.Notes != nil {
app.Notes = input.Notes
}
if input.AppliedAt != nil {
app.AppliedAt = input.AppliedAt
}
if err := r.Store.DB.Save(&app).Error; err != nil {
return nil, err
}
return &app, nil
}
// DeleteJobApplication is the resolver for the deleteJobApplication field.
func (r *mutationResolver) DeleteJobApplication(ctx context.Context, id int) (bool, error) {
if !IsAdminFromCtx(ctx) {
return false, fmt.Errorf("admin access required")
}
if err := r.Store.DB.Delete(&models.JobApplication{}, id).Error; err != nil {
return false, err
}
return true, nil
}
// JobApplications is the resolver for the jobApplications field.
func (r *queryResolver) JobApplications(ctx context.Context) ([]*models.JobApplication, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
var apps []*models.JobApplication
if err := r.Store.DB.Order("created_at desc").Find(&apps).Error; err != nil {
return nil, err
}
return apps, nil
}
// JobApplication is the resolver for the jobApplication field.
func (r *queryResolver) JobApplication(ctx context.Context, id int) (*models.JobApplication, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
var app models.JobApplication
if err := r.Store.DB.First(&app, id).Error; err != nil {
return nil, err
}
return &app, nil
}
// JobApplication returns JobApplicationResolver implementation. // JobApplication returns JobApplicationResolver implementation.
func (r *Resolver) JobApplication() JobApplicationResolver { return &jobApplicationResolver{r} } func (r *Resolver) JobApplication() JobApplicationResolver { return &jobApplicationResolver{r} }

View File

@@ -21,6 +21,19 @@ func (r *messageResolver) AuthorID(ctx context.Context, obj *models.Message) (in
return int(obj.AuthorID), nil return int(obj.AuthorID), 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
}
// Message returns MessageResolver implementation. // Message returns MessageResolver implementation.
func (r *Resolver) Message() MessageResolver { return &messageResolver{r} } func (r *Resolver) Message() MessageResolver { return &messageResolver{r} }

View File

@@ -7,15 +7,111 @@ package graph
import ( import (
"context" "context"
"fmt"
"adam-french.co.uk/backend/graph/model"
"adam-french.co.uk/backend/models" "adam-french.co.uk/backend/models"
) )
// 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
}
// ID is the resolver for the id field. // ID is the resolver for the id field.
func (r *postResolver) ID(ctx context.Context, obj *models.Post) (int, error) { func (r *postResolver) ID(ctx context.Context, obj *models.Post) (int, error) {
return int(obj.ID), nil return int(obj.ID), 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
}
// Post returns PostResolver implementation. // Post returns PostResolver implementation.
func (r *Resolver) Post() PostResolver { return &postResolver{r} } func (r *Resolver) Post() PostResolver { return &postResolver{r} }

View File

@@ -11,6 +11,19 @@ import (
"adam-french.co.uk/backend/models" "adam-french.co.uk/backend/models"
) )
// 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
}
// ID is the resolver for the id field. // ID is the resolver for the id field.
func (r *rowingResolver) ID(ctx context.Context, obj *models.Rowing) (int, error) { func (r *rowingResolver) ID(ctx context.Context, obj *models.Rowing) (int, error) {
return int(obj.ID), nil return int(obj.ID), nil

View File

@@ -5,620 +5,11 @@ package graph
// will be copied through when generating and any unknown code will be moved to the end. // 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 // Code generated by github.com/99designs/gqlgen version v0.17.88
import (
"context"
"fmt"
"net/http"
"adam-french.co.uk/backend/graph/model"
"adam-french.co.uk/backend/models"
"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")
}
if !r.Store.LoginLimiter.Allow(gc.ClientIP()) {
return nil, fmt.Errorf("too many login attempts, please try again later")
}
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.Domain,
true, true,
)
gc.SetCookie(
"refresh_token",
tokens.RefreshToken,
int(r.Store.Auth.Config.RefreshTokenLifetime.Seconds()),
"/",
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, "/", r.Store.Auth.Config.Domain, true, true)
gc.SetCookie("refresh_token", "", -1, "/", r.Store.Auth.Config.Domain, 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.Domain,
true, true,
)
gc.SetCookie(
"refresh_token",
tokens.RefreshToken,
int(r.Store.Auth.Config.RefreshTokenLifetime.Seconds()),
"/",
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
}
// CreateJobApplication is the resolver for the createJobApplication field.
func (r *mutationResolver) CreateJobApplication(ctx context.Context, input model.CreateJobApplicationInput) (*models.JobApplication, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
app := models.JobApplication{
JobTitle: input.JobTitle,
Company: input.Company,
Location: input.Location,
URL: input.URL,
Status: input.Status,
Notes: input.Notes,
AppliedAt: input.AppliedAt,
}
if err := r.Store.DB.Create(&app).Error; err != nil {
return nil, err
}
return &app, nil
}
// UpdateJobApplication is the resolver for the updateJobApplication field.
func (r *mutationResolver) UpdateJobApplication(ctx context.Context, id int, input model.UpdateJobApplicationInput) (*models.JobApplication, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
var app models.JobApplication
if err := r.Store.DB.First(&app, id).Error; err != nil {
return nil, err
}
if input.JobTitle != nil {
app.JobTitle = *input.JobTitle
}
if input.Company != nil {
app.Company = *input.Company
}
if input.Location != nil {
app.Location = input.Location
}
if input.URL != nil {
app.URL = input.URL
}
if input.Status != nil {
app.Status = *input.Status
}
if input.Notes != nil {
app.Notes = input.Notes
}
if input.AppliedAt != nil {
app.AppliedAt = input.AppliedAt
}
if err := r.Store.DB.Save(&app).Error; err != nil {
return nil, err
}
return &app, nil
}
// CreateBookmark is the resolver for the createBookmark field.
func (r *mutationResolver) CreateBookmark(ctx context.Context, input model.CreateBookmarkInput) (*models.Bookmark, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
bookmark := models.Bookmark{Category: input.Category, Name: input.Name, Link: input.Link}
if err := r.Store.DB.Create(&bookmark).Error; err != nil {
return nil, err
}
return &bookmark, nil
}
// DeleteBookmark is the resolver for the deleteBookmark field.
func (r *mutationResolver) DeleteBookmark(ctx context.Context, id int) (*models.Bookmark, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
var bookmark models.Bookmark
if err := r.Store.DB.First(&bookmark, id).Error; err != nil {
return nil, err
}
if err := r.Store.DB.Delete(&bookmark).Error; err != nil {
return nil, err
}
return &bookmark, nil
}
// DeleteJobApplication is the resolver for the deleteJobApplication field.
func (r *mutationResolver) DeleteJobApplication(ctx context.Context, id int) (bool, error) {
if !IsAdminFromCtx(ctx) {
return false, fmt.Errorf("admin access required")
}
if err := r.Store.DB.Delete(&models.JobApplication{}, id).Error; err != nil {
return false, err
}
return true, nil
}
// CreateJobAppReference is the resolver for the createJobAppReference field.
func (r *mutationResolver) CreateJobAppReference(ctx context.Context, input model.CreateJobAppReferenceInput) (*models.JobAppReference, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
ref := models.JobAppReference{
Category: input.Category,
Label: input.Label,
Value: input.Value,
}
if input.SortOrder != nil {
ref.SortOrder = *input.SortOrder
}
if err := r.Store.DB.Create(&ref).Error; err != nil {
return nil, err
}
return &ref, nil
}
// UpdateJobAppReference is the resolver for the updateJobAppReference field.
func (r *mutationResolver) UpdateJobAppReference(ctx context.Context, id int, input model.UpdateJobAppReferenceInput) (*models.JobAppReference, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
var ref models.JobAppReference
if err := r.Store.DB.First(&ref, id).Error; err != nil {
return nil, err
}
if input.Category != nil {
ref.Category = *input.Category
}
if input.Label != nil {
ref.Label = *input.Label
}
if input.Value != nil {
ref.Value = *input.Value
}
if input.SortOrder != nil {
ref.SortOrder = *input.SortOrder
}
if err := r.Store.DB.Save(&ref).Error; err != nil {
return nil, err
}
return &ref, nil
}
// DeleteJobAppReference is the resolver for the deleteJobAppReference field.
func (r *mutationResolver) DeleteJobAppReference(ctx context.Context, id int) (bool, error) {
if !IsAdminFromCtx(ctx) {
return false, fmt.Errorf("admin access required")
}
if err := r.Store.DB.Delete(&models.JobAppReference{}, id).Error; err != nil {
return false, err
}
return true, nil
}
// Users is the resolver for the users field.
func (r *queryResolver) Users(ctx context.Context) ([]*models.User, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
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) {
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")
}
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
}
// 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
}
// Bookmarks is the resolver for the bookmarks field.
func (r *queryResolver) Bookmarks(ctx context.Context) ([]*models.Bookmark, error) {
var bookmarks []models.Bookmark
if err := r.Store.DB.Order("category ASC, created_at ASC").Find(&bookmarks).Error; err != nil {
return nil, err
}
result := make([]*models.Bookmark, len(bookmarks))
for i := range bookmarks {
result[i] = &bookmarks[i]
}
return result, nil
}
// JobApplications is the resolver for the jobApplications field.
func (r *queryResolver) JobApplications(ctx context.Context) ([]*models.JobApplication, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
var apps []*models.JobApplication
if err := r.Store.DB.Order("created_at desc").Find(&apps).Error; err != nil {
return nil, err
}
return apps, nil
}
// JobApplication is the resolver for the jobApplication field.
func (r *queryResolver) JobApplication(ctx context.Context, id int) (*models.JobApplication, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
var app models.JobApplication
if err := r.Store.DB.First(&app, id).Error; err != nil {
return nil, err
}
return &app, nil
}
// JobAppReferences is the resolver for the jobAppReferences field.
func (r *queryResolver) JobAppReferences(ctx context.Context) ([]*models.JobAppReference, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
var refs []*models.JobAppReference
if err := r.Store.DB.Order("category ASC, sort_order ASC, created_at ASC").Find(&refs).Error; err != nil {
return nil, err
}
return refs, nil
}
// Mutation returns MutationResolver implementation. // Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} } func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
// Query returns QueryResolver implementation. // Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
type ( type mutationResolver struct{ *Resolver }
mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver }
queryResolver struct{ *Resolver }
)

View File

@@ -12,3 +12,11 @@ input CreateActivityInput {
name: String! name: String!
link: String link: String
} }
extend type Query {
activities: [Activity!]!
}
extend type Mutation {
createActivity(input: CreateActivityInput!): Activity!
}

View File

@@ -6,3 +6,13 @@ input LoginInput {
type AuthPayload { type AuthPayload {
user: User! user: User!
} }
extend type Query {
me: User
}
extend type Mutation {
login(input: LoginInput!): AuthPayload!
logout: Boolean!
refreshToken: AuthPayload!
}

View File

@@ -12,3 +12,12 @@ input CreateBookmarkInput {
name: String! name: String!
link: String! link: String!
} }
extend type Query {
bookmarks: [Bookmark!]!
}
extend type Mutation {
createBookmark(input: CreateBookmarkInput!): Bookmark!
deleteBookmark(id: ID!): Bookmark!
}

View File

@@ -12,3 +12,11 @@ input CreateFavoriteInput {
name: String! name: String!
link: String link: String
} }
extend type Query {
favorites: [Favorite!]!
}
extend type Mutation {
createFavorite(input: CreateFavoriteInput!): Favorite!
}

View File

@@ -6,3 +6,7 @@ type GiteaFeedItem {
commitMessage: String! commitMessage: String!
createdAt: Time! createdAt: Time!
} }
extend type Query {
giteaFeed: GiteaFeedItem
}

View File

@@ -21,3 +21,13 @@ input UpdateJobAppReferenceInput {
value: String value: String
sortOrder: Int sortOrder: Int
} }
extend type Query {
jobAppReferences: [JobAppReference!]!
}
extend type Mutation {
createJobAppReference(input: CreateJobAppReferenceInput!): JobAppReference!
updateJobAppReference(id: ID!, input: UpdateJobAppReferenceInput!): JobAppReference!
deleteJobAppReference(id: ID!): Boolean!
}

View File

@@ -30,3 +30,14 @@ input UpdateJobApplicationInput {
notes: String notes: String
appliedAt: Time appliedAt: Time
} }
extend type Query {
jobApplications: [JobApplication!]!
jobApplication(id: ID!): JobApplication
}
extend type Mutation {
createJobApplication(input: CreateJobApplicationInput!): JobApplication!
updateJobApplication(id: ID!, input: UpdateJobApplicationInput!): JobApplication!
deleteJobApplication(id: ID!): Boolean!
}

View File

@@ -5,3 +5,7 @@ type Message {
fileUrl: String fileUrl: String
createdAt: Time! createdAt: Time!
} }
extend type Query {
messages: [Message!]!
}

View File

@@ -16,3 +16,14 @@ input UpdatePostInput {
title: String! title: String!
content: String! content: String!
} }
extend type Query {
posts: [Post!]!
post(id: ID!): Post
}
extend type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Post!
}

View File

@@ -7,3 +7,7 @@ type Rowing {
timePer500m: Float! timePer500m: Float!
calories: Float! calories: Float!
} }
extend type Query {
rowingSessions: [Rowing!]!
}

View File

@@ -1,43 +1,4 @@
scalar Time scalar Time
type Query { type Query
users: [User!]! type Mutation
user(id: ID!): User
posts: [Post!]!
post(id: ID!): Post
activities: [Activity!]!
favorites: [Favorite!]!
rowingSessions: [Rowing!]!
messages: [Message!]!
spotifyListening: SpotifyPlaying
spotifyRecent: [SpotifyRecentItem!]
giteaFeed: GiteaFeedItem
steamStatus: SteamStatus
me: User
bookmarks: [Bookmark!]!
jobApplications: [JobApplication!]!
jobApplication(id: ID!): JobApplication
jobAppReferences: [JobAppReference!]!
}
type Mutation {
login(input: LoginInput!): AuthPayload!
logout: Boolean!
refreshToken: AuthPayload!
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Post!
createUser(input: CreateUserInput!): User!
deleteUser(id: ID!): User!
setUserAdmin(id: ID!, admin: Boolean!): User!
createFavorite(input: CreateFavoriteInput!): Favorite!
createActivity(input: CreateActivityInput!): Activity!
createJobApplication(input: CreateJobApplicationInput!): JobApplication!
updateJobApplication(id: ID!, input: UpdateJobApplicationInput!): JobApplication!
createBookmark(input: CreateBookmarkInput!): Bookmark!
deleteBookmark(id: ID!): Bookmark!
deleteJobApplication(id: ID!): Boolean!
createJobAppReference(input: CreateJobAppReferenceInput!): JobAppReference!
updateJobAppReference(id: ID!, input: UpdateJobAppReferenceInput!): JobAppReference!
deleteJobAppReference(id: ID!): Boolean!
}

View File

@@ -26,3 +26,8 @@ type SpotifyRecentItem {
track: SpotifyTrack! track: SpotifyTrack!
playedAt: Time! playedAt: Time!
} }
extend type Query {
spotifyListening: SpotifyPlaying
spotifyRecent: [SpotifyRecentItem!]
}

View File

@@ -10,3 +10,7 @@ type SteamStatus {
online: Boolean! online: Boolean!
recentGames: [SteamGame!]! recentGames: [SteamGame!]!
} }
extend type Query {
steamStatus: SteamStatus
}

View File

@@ -10,3 +10,14 @@ input CreateUserInput {
username: String! username: String!
password: String! password: String!
} }
extend type Query {
users: [User!]!
user(id: ID!): User
}
extend type Mutation {
createUser(input: CreateUserInput!): User!
deleteUser(id: ID!): User!
setUserAdmin(id: ID!, admin: Boolean!): User!
}

View File

@@ -1,58 +1,18 @@
package graph 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 ( import (
"context" "context"
"time" "time"
"adam-french.co.uk/backend/graph/model" "adam-french.co.uk/backend/graph/model"
"github.com/zmb3/spotify/v2" spotify "github.com/zmb3/spotify/v2"
) )
func mapSpotifyImages(images []spotify.Image) []*model.SpotifyImage {
result := make([]*model.SpotifyImage, len(images))
for i, img := range images {
result[i] = &model.SpotifyImage{URL: img.URL}
}
return result
}
func mapSpotifyTrack(track *spotify.FullTrack) *model.SpotifyTrack {
artists := make([]*model.SpotifyArtist, len(track.Artists))
for i, a := range track.Artists {
artists[i] = &model.SpotifyArtist{Name: a.Name}
}
return &model.SpotifyTrack{
Name: track.Name,
Artists: artists,
Album: &model.SpotifyAlbum{
Name: track.Album.Name,
Images: mapSpotifyImages(track.Album.Images),
},
}
}
func mapRecentItems(items []spotify.RecentlyPlayedItem) []*model.SpotifyRecentItem {
result := make([]*model.SpotifyRecentItem, len(items))
for i, item := range items {
artists := make([]*model.SpotifyArtist, len(item.Track.Artists))
for j, a := range item.Track.Artists {
artists[j] = &model.SpotifyArtist{Name: a.Name}
}
result[i] = &model.SpotifyRecentItem{
PlayedAt: item.PlayedAt,
Track: &model.SpotifyTrack{
Name: item.Track.Name,
Artists: artists,
Album: &model.SpotifyAlbum{
Name: item.Track.Album.Name,
Images: mapSpotifyImages(item.Track.Album.Images),
},
},
}
}
return result
}
// SpotifyListening is the resolver for the spotifyListening field. // SpotifyListening is the resolver for the spotifyListening field.
func (r *queryResolver) SpotifyListening(ctx context.Context) (*model.SpotifyPlaying, error) { func (r *queryResolver) SpotifyListening(ctx context.Context) (*model.SpotifyPlaying, error) {
if r.Store.SpotifyClient == nil { if r.Store.SpotifyClient == nil {

View File

@@ -0,0 +1,51 @@
package graph
import (
"adam-french.co.uk/backend/graph/model"
"github.com/zmb3/spotify/v2"
)
func mapSpotifyImages(images []spotify.Image) []*model.SpotifyImage {
result := make([]*model.SpotifyImage, len(images))
for i, img := range images {
result[i] = &model.SpotifyImage{URL: img.URL}
}
return result
}
func mapSpotifyTrack(track *spotify.FullTrack) *model.SpotifyTrack {
artists := make([]*model.SpotifyArtist, len(track.Artists))
for i, a := range track.Artists {
artists[i] = &model.SpotifyArtist{Name: a.Name}
}
return &model.SpotifyTrack{
Name: track.Name,
Artists: artists,
Album: &model.SpotifyAlbum{
Name: track.Album.Name,
Images: mapSpotifyImages(track.Album.Images),
},
}
}
func mapRecentItems(items []spotify.RecentlyPlayedItem) []*model.SpotifyRecentItem {
result := make([]*model.SpotifyRecentItem, len(items))
for i, item := range items {
artists := make([]*model.SpotifyArtist, len(item.Track.Artists))
for j, a := range item.Track.Artists {
artists[j] = &model.SpotifyArtist{Name: a.Name}
}
result[i] = &model.SpotifyRecentItem{
PlayedAt: item.PlayedAt,
Track: &model.SpotifyTrack{
Name: item.Track.Name,
Artists: artists,
Album: &model.SpotifyAlbum{
Name: item.Track.Album.Name,
Images: mapSpotifyImages(item.Track.Album.Images),
},
},
}
}
return result
}

View File

@@ -1,28 +1,18 @@
package graph 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 ( import (
"context" "context"
"fmt"
"time" "time"
"adam-french.co.uk/backend/graph/model" "adam-french.co.uk/backend/graph/model"
"adam-french.co.uk/backend/services" "adam-french.co.uk/backend/services"
) )
func mapSteamGames(games []services.SteamRecentGame) []*model.SteamGame {
result := make([]*model.SteamGame, len(games))
for i, g := range games {
result[i] = &model.SteamGame{
AppID: g.AppID,
Name: g.Name,
Playtime2Weeks: g.Playtime2Weeks,
PlaytimeForever: g.PlaytimeForever,
HeaderImageURL: fmt.Sprintf("https://cdn.akamai.steamstatic.com/steam/apps/%d/header.jpg", g.AppID),
}
}
return result
}
// SteamStatus is the resolver for the steamStatus field. // SteamStatus is the resolver for the steamStatus field.
func (r *queryResolver) SteamStatus(ctx context.Context) (*model.SteamStatus, error) { func (r *queryResolver) SteamStatus(ctx context.Context) (*model.SteamStatus, error) {
if r.Store.SteamAPIKey == "" { if r.Store.SteamAPIKey == "" {

View File

@@ -0,0 +1,22 @@
package graph
import (
"fmt"
"adam-french.co.uk/backend/graph/model"
"adam-french.co.uk/backend/services"
)
func mapSteamGames(games []services.SteamRecentGame) []*model.SteamGame {
result := make([]*model.SteamGame, len(games))
for i, g := range games {
result[i] = &model.SteamGame{
AppID: g.AppID,
Name: g.Name,
Playtime2Weeks: g.Playtime2Weeks,
PlaytimeForever: g.PlaytimeForever,
HeaderImageURL: fmt.Sprintf("https://cdn.akamai.steamstatic.com/steam/apps/%d/header.jpg", g.AppID),
}
}
return result
}

View File

@@ -7,10 +7,106 @@ package graph
import ( import (
"context" "context"
"fmt"
"adam-french.co.uk/backend/graph/model"
"adam-french.co.uk/backend/models" "adam-french.co.uk/backend/models"
"golang.org/x/crypto/bcrypt"
) )
// 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
}
// Users is the resolver for the users field.
func (r *queryResolver) Users(ctx context.Context) ([]*models.User, error) {
if !IsAdminFromCtx(ctx) {
return nil, fmt.Errorf("admin access required")
}
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) {
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")
}
return &user, nil
}
// ID is the resolver for the id field. // ID is the resolver for the id field.
func (r *userResolver) ID(ctx context.Context, obj *models.User) (int, error) { func (r *userResolver) ID(ctx context.Context, obj *models.User) (int, error) {
return int(obj.ID), nil return int(obj.ID), nil