diff --git a/.env.example b/.env.example index b96334a..19c21ad 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,16 @@ -IMAGE_PATH=/tmp -REDIRECT_URI=http://localhost:8081/api/auth/callback -CLIENT_ID= -CLIENT_SECRET= - -# can be either postgres or sqlite -DATABASE_TYPE= - -# database file, stereo.db by default. -SQLITE_FILE= - -# postgres DSN, look at https://gorm.io/docs/connecting_to_the_database.html#PostgreSQL -POSTGRES_DSN= - -# Random secret. Recommended length is 64 characters at minimum. -JWT_SECRET= +IMAGE_PATH=/tmp +REDIRECT_URI=http://localhost:8081/api/auth/callback +CLIENT_ID= +CLIENT_SECRET= + +# can be either postgres or sqlite +DATABASE_TYPE= + +# database file, stereo.db by default. +SQLITE_FILE= + +# postgres DSN, look at https://gorm.io/docs/connecting_to_the_database.html#PostgreSQL +POSTGRES_DSN= + +# Random secret. Recommended length is 64 characters at minimum. +JWT_SECRET= diff --git a/.gitignore b/.gitignore index d17bf15..19a3a6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.env -tmp -*.db +.env +tmp +*.db imgs \ No newline at end of file diff --git a/README.md b/README.md index fb342cc..2c2667c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ -# stereo.cat backend - -written in Go, uses Gin. +# stereo.cat backend + +written in Go, uses Gin. + +## database shit + +Instead of using Discord oAuth as a database, we instead use it as a login source, only using it to source a username/id, avatar data and a secure login/registration flow. +We store these attributes alongside stereo.cat specific attributes in our own database. There is a trade-off however: this means that avatar & username data is not updated in real-time, only when the oauth flow is executed. diff --git a/internal/api/api.go b/internal/api/api.go index 592bc7b..bb61ef1 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -1,12 +1,12 @@ -package api - -import ( - "stereo.cat/backend/internal/api/routes" - "stereo.cat/backend/internal/types" -) - -func Register(cfg *types.StereoConfig) { - api := cfg.Router.Group("/api") - routes.RegisterFileRoutes(cfg, api) - routes.RegisterAuthRoutes(cfg, api) -} +package api + +import ( + "stereo.cat/backend/internal/api/routes" + "stereo.cat/backend/internal/types" +) + +func Register(cfg *types.StereoConfig) { + api := cfg.Router.Group("/api") + routes.RegisterFileRoutes(cfg, api) + routes.RegisterAuthRoutes(cfg, api) +} diff --git a/internal/api/routes/auth.go b/internal/api/routes/auth.go index a6a634f..b4a2f38 100644 --- a/internal/api/routes/auth.go +++ b/internal/api/routes/auth.go @@ -1,41 +1,51 @@ -package routes - -import ( - "net/http" - "time" - - "github.com/gin-gonic/gin" - "stereo.cat/backend/internal/auth" - "stereo.cat/backend/internal/types" -) - -func RegisterAuthRoutes(cfg *types.StereoConfig, api *gin.RouterGroup) { - api.GET("/auth/callback", func(c *gin.Context) { - code := c.Query("code") - - t, err := cfg.Client.ExchangeCode(code) - - if err != nil { - panic(err) - } - - user, err := cfg.Client.GetUser(t) - - if err != nil { - panic(err) - } - - jwt, err := auth.GenerateJWT(cfg.JWTSecret, user, uint64(time.Now().Add(time.Second*time.Duration(t.ExpiresIn)).Unix())) - - if err != nil { - panic(err) - } - - c.String(http.StatusOK, jwt) - }) - - api.GET("/auth/me", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) { - claims, _ := c.Get("claims") - c.JSON(http.StatusOK, claims) - }) -} +package routes + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + "stereo.cat/backend/internal/auth" + "stereo.cat/backend/internal/types" +) + +func RegisterAuthRoutes(cfg *types.StereoConfig, api *gin.RouterGroup) { + api.GET("/auth/callback", func(c *gin.Context) { + code := c.Query("code") + + t, err := cfg.Client.ExchangeCode(code) + + if err != nil { + panic(err) + } + + user, err := cfg.Client.GetUser(t) + + if err != nil { + panic(err) + } + + jwt, err := auth.GenerateJWT(cfg.JWTSecret, user, uint64(time.Now().Add(time.Second*time.Duration(t.ExpiresIn)).Unix())) + + if err != nil { + panic(err) + } + + res := cfg.Database.FirstOrCreate(&user) + + if res.Error != nil { + panic(res.Error) + } + + // TODO: redirect to dashboard + c.JSON(http.StatusOK, gin.H{ + "jwt": jwt, + "known": res.RowsAffected == 0, + }) + }) + + api.GET("/auth/me", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) { + claims, _ := c.Get("claims") + c.JSON(http.StatusOK, claims) + }) +} diff --git a/internal/api/routes/files.go b/internal/api/routes/files.go index db70537..c54489d 100644 --- a/internal/api/routes/files.go +++ b/internal/api/routes/files.go @@ -1,144 +1,144 @@ -package routes - -import ( - "os" - "path/filepath" - "strings" - "time" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" - "stereo.cat/backend/internal/auth" - "stereo.cat/backend/internal/types" -) - -func RegisterFileRoutes(cfg *types.StereoConfig, api *gin.RouterGroup) { - api.POST("/upload", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) { - claims := c.MustGet("claims").(jwt.MapClaims) - user := claims["user"].(auth.User) - - uid := user.ID - if uid == "" { - c.JSON(401, gin.H{"error": "unauthorized"}) - return - } - - file, err := c.FormFile("file") - if err != nil { - c.JSON(400, gin.H{"error": "file is required"}) - return - } - - filePath := filepath.Join(cfg.ImagePath, uid, file.Filename) - - if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { - c.JSON(500, gin.H{"error": "failed to create directory"}) - return - } - - if err := c.SaveUploadedFile(file, filePath); err != nil { - c.JSON(500, gin.H{"error": "failed to save file"}) - return - } - - if file.Size <= 0 { - c.JSON(400, gin.H{"error": "file size must be greater than zero"}) - return - } - - fileMeta := types.File{ - ID: uid + "_" + file.Filename, - Path: filePath, - Owner: uid, - CreatedAt: time.Now(), - Size: file.Size, - } - - if err := cfg.Database.Create(&fileMeta).Error; err != nil { - c.JSON(500, gin.H{"error": "failed to save file metadata"}) - return - } - - c.JSON(200, gin.H{"message": "file uploaded successfully", "file_id": fileMeta.ID}) - }) - - api.DELETE("/delete", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) { - claims := c.MustGet("claims").(jwt.MapClaims) - user := claims["user"].(auth.User) - - uid := user.ID - if uid == "" { - c.JSON(401, gin.H{"error": "unauthorized"}) - return - } - - var response struct { - FileID string `json:"file_id" binding:"required"` - } - - if err := c.ShouldBindJSON(&response); err != nil { - c.JSON(400, gin.H{"error": "file_id is required"}) - return - } - - resfID := response.FileID - if resfID == "" { - c.JSON(400, gin.H{"error": "file_id cannot be empty"}) - return - } - - parts := strings.SplitN(resfID, "_", 2) - if len(parts) != 2 { - c.JSON(400, gin.H{"error": "invalid file_id format"}) - return - } - - fileID, filename := parts[0], parts[1] - if fileID != uid { - c.JSON(403, gin.H{"error": "you can only delete your own files"}) - return - } - - filePath := filepath.Join(cfg.ImagePath, uid, filename) - if err := os.Remove(filePath); err != nil { - c.JSON(500, gin.H{"error": "failed to delete file"}) - return - } - - if err := cfg.Database.Where("id = ?", resfID).Delete(&types.File{}).Error; err != nil { - c.JSON(500, gin.H{"error": "failed to delete file metadata"}) - return - } - - c.JSON(200, gin.H{"message": "file deleted successfully"}) - }) - - api.GET("/:name", func(c *gin.Context) { - name := c.Param("name") - parts := strings.SplitN(name, "_", 2) - if len(parts) != 2 { - c.JSON(400, gin.H{"error": "invalid file name"}) - return - } - uid, filename := parts[0], parts[1] - path := filepath.Join(cfg.ImagePath, uid, filename) - if _, err := os.Stat(path); err != nil { - c.JSON(404, gin.H{"error": "file not found"}) - return - } - c.File(path) - }) - - api.GET("/list", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) { - claims := c.MustGet("claims").(jwt.MapClaims) - user := claims["user"].(auth.User) - - var files []types.File - if err := cfg.Database.Where("owner = ?", user.ID).Find(&files).Error; err != nil { - c.JSON(500, gin.H{"error": "failed to retrieve files"}) - return - } - - c.JSON(200, files) - }) -} +package routes + +import ( + "os" + "path/filepath" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "stereo.cat/backend/internal/auth" + "stereo.cat/backend/internal/types" +) + +func RegisterFileRoutes(cfg *types.StereoConfig, api *gin.RouterGroup) { + api.POST("/upload", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) { + claims := c.MustGet("claims").(jwt.MapClaims) + user := claims["user"].(auth.User) + + uid := user.ID + if uid == "" { + c.JSON(401, gin.H{"error": "unauthorized"}) + return + } + + file, err := c.FormFile("file") + if err != nil { + c.JSON(400, gin.H{"error": "file is required"}) + return + } + + filePath := filepath.Join(cfg.ImagePath, uid, file.Filename) + + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + c.JSON(500, gin.H{"error": "failed to create directory"}) + return + } + + if err := c.SaveUploadedFile(file, filePath); err != nil { + c.JSON(500, gin.H{"error": "failed to save file"}) + return + } + + if file.Size <= 0 { + c.JSON(400, gin.H{"error": "file size must be greater than zero"}) + return + } + + fileMeta := types.File{ + ID: uid + "_" + file.Filename, + Path: filePath, + Owner: uid, + CreatedAt: time.Now(), + Size: file.Size, + } + + if err := cfg.Database.Create(&fileMeta).Error; err != nil { + c.JSON(500, gin.H{"error": "failed to save file metadata"}) + return + } + + c.JSON(200, gin.H{"message": "file uploaded successfully", "file_id": fileMeta.ID}) + }) + + api.DELETE("/delete", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) { + claims := c.MustGet("claims").(jwt.MapClaims) + user := claims["user"].(auth.User) + + uid := user.ID + if uid == "" { + c.JSON(401, gin.H{"error": "unauthorized"}) + return + } + + var response struct { + FileID string `json:"file_id" binding:"required"` + } + + if err := c.ShouldBindJSON(&response); err != nil { + c.JSON(400, gin.H{"error": "file_id is required"}) + return + } + + resfID := response.FileID + if resfID == "" { + c.JSON(400, gin.H{"error": "file_id cannot be empty"}) + return + } + + parts := strings.SplitN(resfID, "_", 2) + if len(parts) != 2 { + c.JSON(400, gin.H{"error": "invalid file_id format"}) + return + } + + fileID, filename := parts[0], parts[1] + if fileID != uid { + c.JSON(403, gin.H{"error": "you can only delete your own files"}) + return + } + + filePath := filepath.Join(cfg.ImagePath, uid, filename) + if err := os.Remove(filePath); err != nil { + c.JSON(500, gin.H{"error": "failed to delete file"}) + return + } + + if err := cfg.Database.Where("id = ?", resfID).Delete(&types.File{}).Error; err != nil { + c.JSON(500, gin.H{"error": "failed to delete file metadata"}) + return + } + + c.JSON(200, gin.H{"message": "file deleted successfully"}) + }) + + api.GET("/:name", func(c *gin.Context) { + name := c.Param("name") + parts := strings.SplitN(name, "_", 2) + if len(parts) != 2 { + c.JSON(400, gin.H{"error": "invalid file name"}) + return + } + uid, filename := parts[0], parts[1] + path := filepath.Join(cfg.ImagePath, uid, filename) + if _, err := os.Stat(path); err != nil { + c.JSON(404, gin.H{"error": "file not found"}) + return + } + c.File(path) + }) + + api.GET("/list", auth.JwtMiddleware(cfg.JWTSecret), func(c *gin.Context) { + claims := c.MustGet("claims").(jwt.MapClaims) + user := claims["user"].(auth.User) + + var files []types.File + if err := cfg.Database.Where("owner = ?", user.ID).Find(&files).Error; err != nil { + c.JSON(500, gin.H{"error": "failed to retrieve files"}) + return + } + + c.JSON(200, files) + }) +} diff --git a/internal/auth/client/client.go b/internal/auth/client/client.go index 3a922bc..661ee7a 100644 --- a/internal/auth/client/client.go +++ b/internal/auth/client/client.go @@ -1,117 +1,117 @@ -package client - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - "stereo.cat/backend/internal/auth" -) - -type Client struct { - RedirectUri string - ClientSecret string - ClientId string -} - -const api = "https://discord.com/api/v10" - -func New(redirectUri, clientId, clientSecret string) Client { - return Client{ - RedirectUri: redirectUri, - ClientId: clientId, - ClientSecret: clientSecret, - } -} - -func (c Client) GetUser(t auth.TokenResponse) (auth.User, error) { - user := auth.User{ - Blacklisted: false, - CreatedAt: time.Now(), - } - - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", api, "users/@me"), nil) - - if err != nil { - return user, nil - } - - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", "Bearer "+t.AccessToken) - - resp, err := http.DefaultClient.Do(req) - - if resp.StatusCode != http.StatusOK { - bodyBytes, err := io.ReadAll(resp.Body) - - if err != nil { - return user, err - } - - return user, errors.New(string(bodyBytes)) - } - - if err != nil { - return user, err - } - - defer resp.Body.Close() - - err = json.NewDecoder(resp.Body).Decode(&user) - - if err != nil { - return user, err - } - - return user, nil -} - -func (c Client) ExchangeCode(code string) (auth.TokenResponse, error) { - var tokenResponse auth.TokenResponse - - requestBody := url.Values{ - "grant_type": {"authorization_code"}, - "code": {code}, - "redirect_uri": {c.RedirectUri}, - } - - req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/%s", api, "/oauth2/token"), strings.NewReader(requestBody.Encode())) - - if err != nil { - return tokenResponse, err - } - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.SetBasicAuth(c.ClientId, c.ClientSecret) - - resp, err := http.DefaultClient.Do(req) - - if resp.StatusCode != http.StatusOK { - bodyBytes, err := io.ReadAll(resp.Body) - - if err != nil { - return tokenResponse, err - } - - return tokenResponse, errors.New(string(bodyBytes)) - } - - if err != nil { - return tokenResponse, err - } - - defer resp.Body.Close() - - err = json.NewDecoder(resp.Body).Decode(&tokenResponse) - - if err != nil { - return tokenResponse, err - } - - return tokenResponse, nil -} +package client + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "stereo.cat/backend/internal/auth" +) + +type Client struct { + RedirectUri string + ClientSecret string + ClientId string +} + +const api = "https://discord.com/api/v10" + +func New(redirectUri, clientId, clientSecret string) Client { + return Client{ + RedirectUri: redirectUri, + ClientId: clientId, + ClientSecret: clientSecret, + } +} + +func (c Client) GetUser(t auth.TokenResponse) (auth.User, error) { + user := auth.User{ + Blacklisted: false, + CreatedAt: time.Now(), + } + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", api, "users/@me"), nil) + + if err != nil { + return user, nil + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", "Bearer "+t.AccessToken) + + resp, err := http.DefaultClient.Do(req) + + if resp.StatusCode != http.StatusOK { + bodyBytes, err := io.ReadAll(resp.Body) + + if err != nil { + return user, err + } + + return user, errors.New(string(bodyBytes)) + } + + if err != nil { + return user, err + } + + defer resp.Body.Close() + + err = json.NewDecoder(resp.Body).Decode(&user) + + if err != nil { + return user, err + } + + return user, nil +} + +func (c Client) ExchangeCode(code string) (auth.TokenResponse, error) { + var tokenResponse auth.TokenResponse + + requestBody := url.Values{ + "grant_type": {"authorization_code"}, + "code": {code}, + "redirect_uri": {c.RedirectUri}, + } + + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/%s", api, "/oauth2/token"), strings.NewReader(requestBody.Encode())) + + if err != nil { + return tokenResponse, err + } + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(c.ClientId, c.ClientSecret) + + resp, err := http.DefaultClient.Do(req) + + if resp.StatusCode != http.StatusOK { + bodyBytes, err := io.ReadAll(resp.Body) + + if err != nil { + return tokenResponse, err + } + + return tokenResponse, errors.New(string(bodyBytes)) + } + + if err != nil { + return tokenResponse, err + } + + defer resp.Body.Close() + + err = json.NewDecoder(resp.Body).Decode(&tokenResponse) + + if err != nil { + return tokenResponse, err + } + + return tokenResponse, nil +} diff --git a/internal/auth/jwt.go b/internal/auth/jwt.go index 1ea6d0f..46cea85 100644 --- a/internal/auth/jwt.go +++ b/internal/auth/jwt.go @@ -1,83 +1,83 @@ -package auth - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" -) - -func GenerateJWT(key string, user User, expiryTimestamp uint64) (string, error) { - claims := Claims{ - User: user, - Exp: expiryTimestamp, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString([]byte(key)) -} - -func invalidAuth(c *gin.Context) { - c.String(http.StatusUnauthorized, "Unauthorized.") - c.Abort() -} - -func JwtMiddleware(secret string) gin.HandlerFunc { - return func(c *gin.Context) { - jwtSplit := strings.Split(c.GetHeader("Authorization"), " ") - - if len(jwtSplit) < 2 || jwtSplit[0] != "Bearer" { - invalidAuth(c) - return - } - - claims, err := ValidateJWT(jwtSplit[1], secret) - if err != nil { - invalidAuth(c) - return - } - - if userClaims, ok := claims["user"].(map[string]interface{}); ok { - userJSON, err := json.Marshal(userClaims) // Convert map to JSON - if err != nil { - invalidAuth(c) - return - } - - var user User - err = json.Unmarshal(userJSON, &user) - if err != nil { - invalidAuth(c) - return - } - - claims["user"] = user - } - - c.Set("claims", claims) - c.Next() - } -} - -func ValidateJWT(jwtString, key string) (jwt.MapClaims, error) { - token, err := jwt.Parse(jwtString, func(token *jwt.Token) (any, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("Invalid signing method!") - } - - return []byte(key), nil - }) - - if err != nil { - return nil, err - } - - if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { - return claims, nil - } - - return nil, fmt.Errorf("Invalid token!") -} +package auth + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +func GenerateJWT(key string, user User, expiryTimestamp uint64) (string, error) { + claims := Claims{ + User: user, + Exp: expiryTimestamp, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(key)) +} + +func invalidAuth(c *gin.Context) { + c.String(http.StatusUnauthorized, "Unauthorized.") + c.Abort() +} + +func JwtMiddleware(secret string) gin.HandlerFunc { + return func(c *gin.Context) { + jwtSplit := strings.Split(c.GetHeader("Authorization"), " ") + + if len(jwtSplit) < 2 || jwtSplit[0] != "Bearer" { + invalidAuth(c) + return + } + + claims, err := ValidateJWT(jwtSplit[1], secret) + if err != nil { + invalidAuth(c) + return + } + + if userClaims, ok := claims["user"].(map[string]interface{}); ok { + userJSON, err := json.Marshal(userClaims) // Convert map to JSON + if err != nil { + invalidAuth(c) + return + } + + var user User + err = json.Unmarshal(userJSON, &user) + if err != nil { + invalidAuth(c) + return + } + + claims["user"] = user + } + + c.Set("claims", claims) + c.Next() + } +} + +func ValidateJWT(jwtString, key string) (jwt.MapClaims, error) { + token, err := jwt.Parse(jwtString, func(token *jwt.Token) (any, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Invalid signing method!") + } + + return []byte(key), nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + return claims, nil + } + + return nil, fmt.Errorf("Invalid token!") +} diff --git a/internal/auth/types.go b/internal/auth/types.go index 8627df7..361cebe 100644 --- a/internal/auth/types.go +++ b/internal/auth/types.go @@ -1,40 +1,40 @@ -package auth - -import ( - "time" - - "github.com/golang-jwt/jwt/v5" -) - -type TokenResponse struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - ExpiresIn int64 `json:"expires_in"` - RefreshToken string `json:"refresh_token"` - Scope string `json:"scope"` -} - -type User struct { - ID string `json:"id" gorm:"primaryKey"` - Username string `json:"username"` - Blacklisted bool `json:"blacklisted"` - Email string `json:"email"` - CreatedAt time.Time `json:"created_at"` -} - -type AvatarDecorationData struct { - Asset string - SkuID string -} - -type ExchangeCodeRequest struct { - GrantType string `json:"grant_type"` - Code string `json:"code"` - RedirectUri string `json:"redirect_uri"` -} - -type Claims struct { - User User `json:"user"` - Exp uint64 `json:"exp"` - jwt.RegisteredClaims -} +package auth + +import ( + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type TokenResponse struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int64 `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + Scope string `json:"scope"` +} + +type User struct { + ID string `json:"id" gorm:"primaryKey"` + Username string `json:"username"` + Blacklisted bool `json:"blacklisted"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` +} + +type AvatarDecorationData struct { + Asset string + SkuID string +} + +type ExchangeCodeRequest struct { + GrantType string `json:"grant_type"` + Code string `json:"code"` + RedirectUri string `json:"redirect_uri"` +} + +type Claims struct { + User User `json:"user"` + Exp uint64 `json:"exp"` + jwt.RegisteredClaims +} diff --git a/internal/types/types.go b/internal/types/types.go index 99e5186..505c1fe 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1,31 +1,31 @@ -package types - -import ( - "time" - - "github.com/gin-gonic/gin" - "gorm.io/gorm" - "stereo.cat/backend/internal/auth/client" -) - -type Route struct { - Path string - Method string - Exec func(cfg *StereoConfig) gin.HandlerFunc -} - -type StereoConfig struct { - ImagePath string - Router *gin.Engine - Client client.Client - Database *gorm.DB - JWTSecret string -} - -type File struct { - ID string `gorm:"primaryKey"` - Path string `gorm:"not null;index"` - Owner string `gorm:"not null;index"` - Size int64 `gorm:"not null;type:bigint"` - CreatedAt time.Time `gorm:"autoCreateTime"` -} +package types + +import ( + "time" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + "stereo.cat/backend/internal/auth/client" +) + +type Route struct { + Path string + Method string + Exec func(cfg *StereoConfig) gin.HandlerFunc +} + +type StereoConfig struct { + ImagePath string + Router *gin.Engine + Client client.Client + Database *gorm.DB + JWTSecret string +} + +type File struct { + ID string `gorm:"primaryKey"` + Path string `gorm:"not null;index"` + Owner string `gorm:"not null;index"` + Size int64 `gorm:"not null;type:bigint"` + CreatedAt time.Time `gorm:"autoCreateTime"` +} diff --git a/main.go b/main.go index 54351ce..92466c0 100644 --- a/main.go +++ b/main.go @@ -1,91 +1,91 @@ -package main - -import ( - "errors" - "fmt" - "log" - "os" - - "github.com/gin-gonic/gin" - "github.com/joho/godotenv" - "gorm.io/driver/postgres" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "stereo.cat/backend/internal/api" - "stereo.cat/backend/internal/auth" - "stereo.cat/backend/internal/auth/client" - "stereo.cat/backend/internal/types" -) - -func getEnv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - return fallback -} - -func requireEnv(key string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - panic(errors.New(fmt.Sprintf("Environment variable %s is required but not specified. Exiting...", key))) -} - -func main() { - _ = godotenv.Load() - - databaseType := getEnv("DATABASE_TYPE", "sqlite") - sqliteFile := getEnv("SQLITE_FILE", "stereo.db") - imagePath := getEnv("IMAGE_PATH", os.TempDir()) - - if _, err := os.Stat(imagePath); err != nil { - if os.IsNotExist(err) { - if err := os.MkdirAll(imagePath, os.ModePerm); err != nil { - log.Fatal(err) - } - } - } - - c := types.StereoConfig{ - Router: gin.Default(), - ImagePath: imagePath, - Client: client.New( - requireEnv("REDIRECT_URI"), - requireEnv("CLIENT_ID"), - requireEnv("CLIENT_SECRET"), - ), - JWTSecret: requireEnv("JWT_SECRET"), - } - - switch databaseType { - case "sqlite": - db, err := gorm.Open(sqlite.Open(sqliteFile), &gorm.Config{}) - - c.Database = db - - if err != nil { - panic(err) - } - - break - - case "postgres": - db, err := gorm.Open(postgres.Open(requireEnv("POSTGRES_DSN")), &gorm.Config{}) - - c.Database = db - - if err != nil { - panic(err) - } - - break - default: - panic(errors.New("Invalid database type was specified.")) - } - - c.Database.AutoMigrate(&auth.User{}, &types.File{}) - - api.Register(&c) - fmt.Printf("Running on port %s\n", getEnv("PORT", "8080")) - c.Router.Run() -} +package main + +import ( + "errors" + "fmt" + "log" + "os" + + "github.com/gin-gonic/gin" + "github.com/joho/godotenv" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "stereo.cat/backend/internal/api" + "stereo.cat/backend/internal/auth" + "stereo.cat/backend/internal/auth/client" + "stereo.cat/backend/internal/types" +) + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} + +func requireEnv(key string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + panic(errors.New(fmt.Sprintf("Environment variable %s is required but not specified. Exiting...", key))) +} + +func main() { + _ = godotenv.Load() + + databaseType := getEnv("DATABASE_TYPE", "sqlite") + sqliteFile := getEnv("SQLITE_FILE", "stereo.db") + imagePath := getEnv("IMAGE_PATH", os.TempDir()) + + if _, err := os.Stat(imagePath); err != nil { + if os.IsNotExist(err) { + if err := os.MkdirAll(imagePath, os.ModePerm); err != nil { + log.Fatal(err) + } + } + } + + c := types.StereoConfig{ + Router: gin.Default(), + ImagePath: imagePath, + Client: client.New( + requireEnv("REDIRECT_URI"), + requireEnv("CLIENT_ID"), + requireEnv("CLIENT_SECRET"), + ), + JWTSecret: requireEnv("JWT_SECRET"), + } + + switch databaseType { + case "sqlite": + db, err := gorm.Open(sqlite.Open(sqliteFile), &gorm.Config{}) + + c.Database = db + + if err != nil { + panic(err) + } + + break + + case "postgres": + db, err := gorm.Open(postgres.Open(requireEnv("POSTGRES_DSN")), &gorm.Config{}) + + c.Database = db + + if err != nil { + panic(err) + } + + break + default: + panic(errors.New("Invalid database type was specified.")) + } + + c.Database.AutoMigrate(&auth.User{}, &types.File{}) + + api.Register(&c) + fmt.Printf("Running on port %s\n", getEnv("PORT", "8080")) + c.Router.Run() +}