Fix security vulnerabilities across backend, frontend, and infra
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m44s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m44s
- Fix auth bypass in UpdatePost/DeletePost (missing return after auth check) - Remove Spotify access token from callback response - Replace internal error messages with generic responses in all handlers - Harden GraphQL: complexity limit, disable playground/introspection in prod - Add security headers (X-Frame-Options, HSTS, etc.) to nginx - Disable Hasura console/dev mode in production - Add DOMPurify sanitization to Markdown component - Fix cookie removal to use correct domain/path from auth config - Fix nil dereference in rowing handler when Claude API errors - Fix wildcard CORS on stamp endpoint - Pin nginx and certbot Docker image versions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"adam-french.co.uk/backend/models"
|
||||
@@ -16,7 +17,8 @@ type CreateActivityInput struct {
|
||||
func (store *Store) GetActivity(ctx *gin.Context) {
|
||||
var activitys []models.Activity
|
||||
if err := store.DB.Order("Created_At DESC").Find(&activitys).Error; err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, err.Error())
|
||||
log.Println(err)
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, activitys)
|
||||
@@ -25,14 +27,15 @@ func (store *Store) GetActivity(ctx *gin.Context) {
|
||||
func (store *Store) CreateActivity(ctx *gin.Context) {
|
||||
var input CreateActivityInput
|
||||
if err := ctx.ShouldBindBodyWithJSON(&input); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, err.Error())
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
activity := models.Activity{Type: input.Type, Name: input.Name, Link: input.Link}
|
||||
tx := store.DB.Create(&activity)
|
||||
if tx.Error != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, tx.Error.Error())
|
||||
log.Println(tx.Error)
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"adam-french.co.uk/backend/models"
|
||||
@@ -12,13 +13,13 @@ import (
|
||||
func (store *Store) AuthMiddlewear(ctx *gin.Context) {
|
||||
access_token, err := ctx.Cookie("access_token")
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(401, err.Error())
|
||||
ctx.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := store.Auth.VerifyJWT(access_token)
|
||||
if err != nil {
|
||||
ctx.AbortWithStatusJSON(401, err.Error())
|
||||
ctx.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -58,13 +59,13 @@ func (store *Store) CheckToken(ctx *gin.Context) {
|
||||
|
||||
claims, err := store.Auth.VerifyJWT(access_token)
|
||||
if err != nil {
|
||||
ctx.JSON(401, err.Error())
|
||||
ctx.JSON(401, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
userIDF, ok := (*claims)["id"].(float64)
|
||||
if !ok {
|
||||
ctx.JSON(401, gin.H{"error": "claims does not contain id"})
|
||||
ctx.JSON(401, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
userID := uint(userIDF)
|
||||
@@ -72,8 +73,9 @@ func (store *Store) CheckToken(ctx *gin.Context) {
|
||||
user := models.User{ID: userID}
|
||||
tx := store.DB.First(&user)
|
||||
if tx.Error != nil {
|
||||
ctx.JSON(http.StatusNotFound, tx.Error.Error())
|
||||
removeCookies(ctx)
|
||||
log.Println(tx.Error)
|
||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
store.removeCookies(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -83,13 +85,13 @@ func (store *Store) CheckToken(ctx *gin.Context) {
|
||||
func (store *Store) RefreshToken(ctx *gin.Context) {
|
||||
refreshToken, err := ctx.Cookie("refresh_token")
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusUnauthorized, err.Error())
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := store.Auth.VerifyJWT(refreshToken)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusUnauthorized, err.Error())
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,14 +105,16 @@ func (store *Store) RefreshToken(ctx *gin.Context) {
|
||||
user := models.User{ID: userID}
|
||||
tx := store.DB.First(&user)
|
||||
if tx.Error != nil {
|
||||
ctx.JSON(http.StatusNotFound, tx.Error.Error())
|
||||
removeCookies(ctx)
|
||||
log.Println(tx.Error)
|
||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
store.removeCookies(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
tokens, err := store.Auth.GenerateJWT(&user)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, err.Error())
|
||||
log.Println(err)
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -138,7 +142,7 @@ func (store *Store) RefreshToken(ctx *gin.Context) {
|
||||
func (store *Store) Login(ctx *gin.Context) {
|
||||
var input UserCredentials
|
||||
if err := ctx.ShouldBindBodyWithJSON(&input); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, err.Error())
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -155,7 +159,8 @@ func (store *Store) Login(ctx *gin.Context) {
|
||||
|
||||
tokens, err := store.Auth.GenerateJWT(&user)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, err.Error())
|
||||
log.Println(err)
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -181,27 +186,27 @@ func (store *Store) Login(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
func (store *Store) Logout(ctx *gin.Context) {
|
||||
removeCookies(ctx)
|
||||
store.removeCookies(ctx)
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
func removeCookies(ctx *gin.Context) {
|
||||
func (store *Store) removeCookies(ctx *gin.Context) {
|
||||
ctx.SetSameSite(http.SameSiteLaxMode)
|
||||
ctx.SetCookie(
|
||||
"access_token",
|
||||
"",
|
||||
-1,
|
||||
"",
|
||||
"",
|
||||
store.Auth.Config.Endpoint,
|
||||
store.Auth.Config.Domain,
|
||||
true, true,
|
||||
)
|
||||
ctx.SetCookie(
|
||||
"refresh_token",
|
||||
"",
|
||||
-1,
|
||||
"",
|
||||
"",
|
||||
store.Auth.Config.Endpoint,
|
||||
store.Auth.Config.Domain,
|
||||
true, true,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"adam-french.co.uk/backend/models"
|
||||
@@ -16,7 +17,8 @@ type CreateFavoriteInput struct {
|
||||
func (store *Store) GetFavorites(ctx *gin.Context) {
|
||||
var favorites []models.Favorite
|
||||
if err := store.DB.Order("Created_At DESC").Find(&favorites).Error; err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, err.Error())
|
||||
log.Println(err)
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, favorites)
|
||||
@@ -25,14 +27,15 @@ func (store *Store) GetFavorites(ctx *gin.Context) {
|
||||
func (store *Store) CreateFavorite(ctx *gin.Context) {
|
||||
var input CreateFavoriteInput
|
||||
if err := ctx.ShouldBindBodyWithJSON(&input); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, err.Error())
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
favorite := models.Favorite{Type: input.Type, Name: input.Name, Link: input.Link}
|
||||
tx := store.DB.Create(&favorite)
|
||||
if tx.Error != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, tx.Error.Error())
|
||||
log.Println(tx.Error)
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -17,7 +18,8 @@ type CreatePostInput struct {
|
||||
func (store *Store) GetPosts(ctx *gin.Context) {
|
||||
var posts []models.Post
|
||||
if err := store.DB.Preload("Author").Order("Created_At DESC").Find(&posts).Error; err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, err.Error())
|
||||
log.Println(err)
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, posts)
|
||||
@@ -34,7 +36,8 @@ func (store *Store) GetPost(ctx *gin.Context) {
|
||||
|
||||
post := models.Post{ID: uint(postID)}
|
||||
if err := store.DB.First(&post).Error; err != nil {
|
||||
ctx.JSON(http.StatusNotFound, err.Error())
|
||||
log.Println(err)
|
||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -44,25 +47,25 @@ func (store *Store) GetPost(ctx *gin.Context) {
|
||||
func (store *Store) CreatePost(ctx *gin.Context) {
|
||||
var input CreatePostInput
|
||||
if err := ctx.ShouldBindBodyWithJSON(&input); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, err.Error())
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
claimsVal, ok := ctx.Get("userClaims")
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "user claims could not be found"})
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := claimsVal.(*jwt.MapClaims)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "invalid claims"})
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
|
||||
userIDF, ok := (*claims)["id"].(float64)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user id in claims"})
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
userID := uint(userIDF)
|
||||
@@ -71,7 +74,8 @@ func (store *Store) CreatePost(ctx *gin.Context) {
|
||||
post := models.Post{Title: input.Title, Content: input.Content, AuthorID: userID}
|
||||
tx := store.DB.Create(&post)
|
||||
if tx.Error != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, tx.Error.Error())
|
||||
log.Println(tx.Error)
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -82,36 +86,38 @@ func (store *Store) UpdatePost(ctx *gin.Context) {
|
||||
postID := ctx.Param("id")
|
||||
var post models.Post
|
||||
if err := store.DB.First(&post, postID).Error; err != nil {
|
||||
ctx.JSON(http.StatusNotFound, err.Error())
|
||||
log.Println(err)
|
||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
|
||||
claimsVal, ok := ctx.Get("userClaims")
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "user claims could not be found"})
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := claimsVal.(*jwt.MapClaims)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "invalid claims"})
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
|
||||
userIDF, ok := (*claims)["id"].(float64)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user id in claims"})
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
userID := uint(userIDF)
|
||||
|
||||
if !(userID == post.AuthorID) {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "user and post author id missmatch"})
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
var input CreatePostInput
|
||||
if err := ctx.ShouldBindBodyWithJSON(&input); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, err.Error())
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -119,7 +125,8 @@ func (store *Store) UpdatePost(ctx *gin.Context) {
|
||||
post.Content = input.Content
|
||||
tx := store.DB.Save(&post)
|
||||
if tx.Error != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, tx.Error.Error())
|
||||
log.Println(tx.Error)
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -130,31 +137,33 @@ func (store *Store) DeletePost(ctx *gin.Context) {
|
||||
postID := ctx.Param("id")
|
||||
var post models.Post
|
||||
if err := store.DB.First(&post, postID).Error; err != nil {
|
||||
ctx.JSON(http.StatusNotFound, err.Error())
|
||||
log.Println(err)
|
||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
|
||||
claimsVal, ok := ctx.Get("userClaims")
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "user claims could not be found"})
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := claimsVal.(*jwt.MapClaims)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "invalid claims"})
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
|
||||
userIDF, ok := (*claims)["id"].(float64)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user id in claims"})
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
userID := uint(userIDF)
|
||||
|
||||
if !(userID == post.AuthorID) {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "user and post author id missmatch"})
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
store.DB.Delete(&post)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -24,7 +25,8 @@ type ExtractedRowingData struct {
|
||||
func (store *Store) GetRowing(ctx *gin.Context) {
|
||||
var rowing []models.Rowing
|
||||
if err := store.DB.Order("Created_At DESC").Find(&rowing).Error; err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, err.Error())
|
||||
log.Println(err)
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, rowing)
|
||||
@@ -118,18 +120,13 @@ No text, no markdown, no explanation. Just the JSON object.`),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
"raw": message.Content[0].Text,
|
||||
"error": err.Error(),
|
||||
})
|
||||
log.Println(err)
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to process image"})
|
||||
return
|
||||
}
|
||||
|
||||
if len(message.Content) == 0 {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
"raw": message.Content[0].Text,
|
||||
"error": "empty response from Claude",
|
||||
})
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "empty response from image processor"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -144,11 +141,8 @@ No text, no markdown, no explanation. Just the JSON object.`),
|
||||
|
||||
err = json.Unmarshal([]byte(raw), &extractedData)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to parse JSON response",
|
||||
"detail": err.Error(),
|
||||
"raw": raw,
|
||||
})
|
||||
log.Println(err)
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to parse image data"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -17,12 +18,14 @@ func (store *Store) CompleteSpotifyAuth(ctx *gin.Context) {
|
||||
|
||||
token, err := store.SpotifyAuth.Token(c, state, ctx.Request)
|
||||
if err != nil {
|
||||
ctx.String(http.StatusInternalServerError, "Couldn't get token: %v", err)
|
||||
log.Println(err)
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "authentication failed"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.SaveSpotifyToken(services.SPOTIFY_TOKEN_JSON_PATH, token); err != nil {
|
||||
ctx.String(http.StatusInternalServerError, "Failed to save token: %v", err)
|
||||
log.Println(err)
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -32,9 +35,6 @@ func (store *Store) CompleteSpotifyAuth(ctx *gin.Context) {
|
||||
|
||||
ctx.JSON(http.StatusOK, gin.H{
|
||||
"message": "Authentication successful",
|
||||
"token": token.AccessToken,
|
||||
"type": token.TokenType,
|
||||
"expiry": token.Expiry,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ func (store *Store) ListeningTo(ctx *gin.Context) {
|
||||
|
||||
playing, err := store.SpotifyClient.PlayerCurrentlyPlaying(c)
|
||||
if err != nil {
|
||||
ctx.JSON(500, gin.H{"error": err.Error()})
|
||||
log.Println(err)
|
||||
ctx.JSON(500, gin.H{"error": "failed to fetch currently playing"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -70,7 +71,8 @@ func (store *Store) RecentlyPlayed(ctx *gin.Context) {
|
||||
|
||||
played, err := store.SpotifyClient.PlayerRecentlyPlayedOpt(ctx, &opts)
|
||||
if err != nil {
|
||||
ctx.JSON(500, gin.H{"error": err.Error()})
|
||||
log.Println(err)
|
||||
ctx.JSON(500, gin.H{"error": "failed to fetch recently played"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/99designs/gqlgen/graphql/handler/extension"
|
||||
"github.com/99designs/gqlgen/graphql/handler/lru"
|
||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||
"github.com/99designs/gqlgen/graphql/playground"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/vektah/gqlparser/v2/ast"
|
||||
|
||||
"adam-french.co.uk/backend/graph"
|
||||
"adam-french.co.uk/backend/handlers"
|
||||
@@ -129,15 +133,27 @@ func main() {
|
||||
r.GET("/notes/*path", store.GetNoteFile)
|
||||
|
||||
// GRAPHQL
|
||||
gqlSrv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{
|
||||
gqlSrv := handler.New(graph.NewExecutableSchema(graph.Config{
|
||||
Resolvers: &graph.Resolver{Store: &store},
|
||||
}))
|
||||
gqlSrv.AddTransport(transport.Websocket{KeepAlivePingInterval: 10 * time.Second})
|
||||
gqlSrv.AddTransport(transport.Options{})
|
||||
gqlSrv.AddTransport(transport.GET{})
|
||||
gqlSrv.AddTransport(transport.POST{})
|
||||
gqlSrv.AddTransport(transport.MultipartForm{})
|
||||
gqlSrv.SetQueryCache(lru.New[*ast.QueryDocument](1000))
|
||||
gqlSrv.Use(extension.FixedComplexityLimit(200))
|
||||
if os.Getenv("GQL_INTROSPECTION") == "true" {
|
||||
gqlSrv.Use(extension.Introspection{})
|
||||
}
|
||||
r.POST("/graphql", graph.AuthContextMiddleware(auth), func(c *gin.Context) {
|
||||
gqlSrv.ServeHTTP(c.Writer, c.Request)
|
||||
})
|
||||
if os.Getenv("GQL_PLAYGROUND") == "true" {
|
||||
r.GET("/graphql", func(c *gin.Context) {
|
||||
playground.Handler("GraphQL Playground", "/graphql").ServeHTTP(c.Writer, c.Request)
|
||||
})
|
||||
}
|
||||
|
||||
// HELLO WORLD
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
|
||||
@@ -9,6 +9,8 @@ services:
|
||||
backend:
|
||||
environment:
|
||||
- SPOTIFY_REDIRECT_URI=https://localhost/api/spotify/callback
|
||||
- GQL_PLAYGROUND=true
|
||||
- GQL_INTROSPECTION=true
|
||||
nginx:
|
||||
environment:
|
||||
- DEV_MODE=true
|
||||
@@ -16,6 +18,10 @@ services:
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
hasura:
|
||||
environment:
|
||||
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
|
||||
HASURA_GRAPHQL_DEV_MODE: "true"
|
||||
certbot:
|
||||
profiles:
|
||||
- disabled
|
||||
|
||||
@@ -43,7 +43,7 @@ services:
|
||||
- vue_dist:/etc/nginx/html
|
||||
|
||||
certbot:
|
||||
image: certbot/certbot
|
||||
image: certbot/certbot:v3.1.0
|
||||
container_name: certbot
|
||||
volumes:
|
||||
- ./certbot/entrypoint.sh:/entrypoint.sh
|
||||
@@ -95,8 +95,8 @@ services:
|
||||
environment:
|
||||
HASURA_GRAPHQL_DATABASE_URL: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
|
||||
HASURA_GRAPHQL_ADMIN_SECRET: "${HASURA_GRAPHQL_ADMIN_SECRET}"
|
||||
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
|
||||
HASURA_GRAPHQL_DEV_MODE: "true"
|
||||
HASURA_GRAPHQL_ENABLE_CONSOLE: "false"
|
||||
HASURA_GRAPHQL_DEV_MODE: "false"
|
||||
HASURA_GRAPHQL_ENABLED_LOG_TYPES: "startup, http-log, webhook-log, websocket-log, query-log"
|
||||
|
||||
icecast2:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM nginx:latest
|
||||
FROM nginx:1.27
|
||||
RUN rm -rf /etc/nginx/html/* && \
|
||||
apt-get update && apt-get install -y gettext-base openssl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -77,6 +77,11 @@ http {
|
||||
ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem;
|
||||
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
||||
|
||||
# Vite hashed assets - immutable, cache 1 year
|
||||
location /assets/ {
|
||||
expires 1y;
|
||||
@@ -117,7 +122,7 @@ http {
|
||||
}
|
||||
|
||||
location = /img/stamps/mine.gif {
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Origin "https://www.$DOMAIN";
|
||||
expires 7d;
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
|
||||
17
vue/package-lock.json
generated
17
vue/package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"axios": "^1.13.2",
|
||||
"dompurify": "^3.3.3",
|
||||
"katex": "^0.16.27",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-wikilinks": "^1.4.0",
|
||||
@@ -1633,6 +1634,13 @@
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
@@ -2226,6 +2234,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
|
||||
"integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"axios": "^1.13.2",
|
||||
"dompurify": "^3.3.3",
|
||||
"katex": "^0.16.27",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-wikilinks": "^1.4.0",
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
<script setup>
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { katex } from "@mdit/plugin-katex";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
const mdIt = MarkdownIt().use(katex);
|
||||
//.use(wiki);
|
||||
|
||||
const props = defineProps({
|
||||
source: String,
|
||||
});
|
||||
|
||||
function renderMarkdown(source) {
|
||||
return DOMPurify.sanitize(mdIt.render(source));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-html="mdIt.render(props.source)"
|
||||
v-html="renderMarkdown(props.source)"
|
||||
class="flex flex-col items-center"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user