From ccf644acef9959b2285ce3cd23ab620ed9af4960 Mon Sep 17 00:00:00 2001 From: wisplite Date: Wed, 29 Oct 2025 00:24:50 -0500 Subject: [PATCH] a commit that changes nothing? --- .vscode/settings.json | 40 +++++----- README.md | 90 +++++++++++------------ backend/cmd/server/main.go | 42 +++++------ backend/internal/db/db.go | 76 +++++++++---------- backend/internal/models/accessTokens.go | 18 ++--- backend/internal/models/album.go | 34 ++++----- backend/internal/models/user.go | 26 +++---- backend/internal/routes/album.go | 40 +++++----- backend/internal/routes/routes.go | 16 ++-- backend/internal/routes/user.go | 64 ++++++++-------- backend/internal/services/accessTokens.go | 48 ++++++------ backend/internal/services/album.go | 50 ++++++------- backend/internal/services/user.go | 90 +++++++++++------------ 13 files changed, 317 insertions(+), 317 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 41a815f..32d9af2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,21 +1,21 @@ -{ - "go.useLanguageServer": true, - "go.formatTool": "goimports", - "go.lintOnSave": "package", - "go.vetOnSave": "package", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": "never", - "source.fixAll": "never" - }, - "[go]": { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": "never", - "source.fixAll": "never" - } - }, - "go.buildOnSave": "off", - "go.lintTool": "golint", - "go.toolsManagement.autoUpdate": true +{ + "go.useLanguageServer": true, + "go.formatTool": "goimports", + "go.lintOnSave": "package", + "go.vetOnSave": "package", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "never", + "source.fixAll": "never" + }, + "[go]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "never", + "source.fixAll": "never" + } + }, + "go.buildOnSave": "off", + "go.lintTool": "golint", + "go.toolsManagement.autoUpdate": true } \ No newline at end of file diff --git a/README.md b/README.md index 341e2fe..9aa1a31 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,46 @@ -## Raster: A Simple and Powerful Self-Hosted Photo/Video Management Solution - -Raster is a free and open-source photo/video management solution, with a focus on simplicity where it matters. - -## Why Raster? -I started developing Raster because I found the other self-hosted galleries either lacking in features or difficult to install and maintain. - -I wanted a solution that was: -- Lightweight and easy to run -- Simple, clean UI -- Albums, content tags, and descriptions -- Public albums for showcasing work -- Private shared albums with multi-user uploads -- Comments and likes in shared albums -- Proper panorama support -- Modern tech stack - -## Quick start - -As of this moment, Raster is VERY early in development, and as such production builds are not ready yet. The instructions below are for testing and development. Do not use Raster for production until more progress has been made. - -Backend (API): -```bash -cd backend -go run ./cmd/server -# Server runs on http://localhost:8080 -# A SQLite database (raster.db) will be created automatically in the backend directory. -``` - -Frontend (web app): -```bash -cd frontend -npm i -npm run dev -# App runs on http://localhost:5173 -``` - -## Project structure -- `backend/`: Go API using Gin + GORM (SQLite default) -- `frontend/`: React app (Vite, Tailwind) - -## Status -In early development. Expect rapid updates. - -## Contributing +## Raster: A Simple and Powerful Self-Hosted Photo/Video Management Solution + +Raster is a free and open-source photo/video management solution, with a focus on simplicity where it matters. + +## Why Raster? +I started developing Raster because I found the other self-hosted galleries either lacking in features or difficult to install and maintain. + +I wanted a solution that was: +- Lightweight and easy to run +- Simple, clean UI +- Albums, content tags, and descriptions +- Public albums for showcasing work +- Private shared albums with multi-user uploads +- Comments and likes in shared albums +- Proper panorama support +- Modern tech stack + +## Quick start + +As of this moment, Raster is VERY early in development, and as such production builds are not ready yet. The instructions below are for testing and development. Do not use Raster for production until more progress has been made. + +Backend (API): +```bash +cd backend +go run ./cmd/server +# Server runs on http://localhost:8080 +# A SQLite database (raster.db) will be created automatically in the backend directory. +``` + +Frontend (web app): +```bash +cd frontend +npm i +npm run dev +# App runs on http://localhost:5173 +``` + +## Project structure +- `backend/`: Go API using Gin + GORM (SQLite default) +- `frontend/`: React app (Vite, Tailwind) + +## Status +In early development. Expect rapid updates. + +## Contributing Issues and PRs are welcome. \ No newline at end of file diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 133d501..b4d1b4b 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -1,21 +1,21 @@ -package main - -import ( - "log" - - "github.com/gin-gonic/gin" - "github.com/wisplite/raster/internal/db" - "github.com/wisplite/raster/internal/routes" -) - -func main() { - if !db.Init() { - log.Fatal("failed to initialize database") - } - - r := gin.Default() - - routes.RegisterRoutes(r) - - r.Run(":8080") -} +package main + +import ( + "log" + + "github.com/gin-gonic/gin" + "github.com/wisplite/raster/internal/db" + "github.com/wisplite/raster/internal/routes" +) + +func main() { + if !db.Init() { + log.Fatal("failed to initialize database") + } + + r := gin.Default() + + routes.RegisterRoutes(r) + + r.Run(":8080") +} diff --git a/backend/internal/db/db.go b/backend/internal/db/db.go index a6a2db9..14e5a91 100644 --- a/backend/internal/db/db.go +++ b/backend/internal/db/db.go @@ -1,38 +1,38 @@ -package db - -import ( - "log" - - "github.com/glebarez/sqlite" - "github.com/wisplite/raster/internal/models" - "gorm.io/gorm" -) - -var db *gorm.DB - -func Init() bool { - database, err := gorm.Open(sqlite.Open("raster.db"), &gorm.Config{}) - if err != nil { - log.Fatal("failed to connect database: ", err) - return false - } - - // Run migrations - err = database.AutoMigrate( - &models.Album{}, - &models.User{}, - &models.AccessToken{}, - ) - if err != nil { - log.Fatal("failed to migrate database: ", err) - return false - } - - db = database - - return true -} - -func GetDB() *gorm.DB { - return db -} +package db + +import ( + "log" + + "github.com/glebarez/sqlite" + "github.com/wisplite/raster/internal/models" + "gorm.io/gorm" +) + +var db *gorm.DB + +func Init() bool { + database, err := gorm.Open(sqlite.Open("raster.db"), &gorm.Config{}) + if err != nil { + log.Fatal("failed to connect database: ", err) + return false + } + + // Run migrations + err = database.AutoMigrate( + &models.Album{}, + &models.User{}, + &models.AccessToken{}, + ) + if err != nil { + log.Fatal("failed to migrate database: ", err) + return false + } + + db = database + + return true +} + +func GetDB() *gorm.DB { + return db +} diff --git a/backend/internal/models/accessTokens.go b/backend/internal/models/accessTokens.go index 64552d6..2acc17d 100644 --- a/backend/internal/models/accessTokens.go +++ b/backend/internal/models/accessTokens.go @@ -1,9 +1,9 @@ -package models - -import "time" - -type AccessToken struct { - Token string `gorm:"primaryKey"` - UserID string `gorm:"not null"` - Expires time.Time `gorm:"not null"` -} +package models + +import "time" + +type AccessToken struct { + Token string `gorm:"primaryKey"` + UserID string `gorm:"not null"` + Expires time.Time `gorm:"not null"` +} diff --git a/backend/internal/models/album.go b/backend/internal/models/album.go index f7d31d2..714f8be 100644 --- a/backend/internal/models/album.go +++ b/backend/internal/models/album.go @@ -1,17 +1,17 @@ -package models - -import ( - "time" - - "gorm.io/datatypes" -) - -type Album struct { - ID string `gorm:"primaryKey"` - Title string `gorm:"not null"` - Description string `gorm:"not null"` - Tags datatypes.JSON `gorm:"type:json"` - Private bool `gorm:"not null"` - CreatedAt time.Time - UpdatedAt time.Time -} +package models + +import ( + "time" + + "gorm.io/datatypes" +) + +type Album struct { + ID string `gorm:"primaryKey"` + Title string `gorm:"not null"` + Description string `gorm:"not null"` + Tags datatypes.JSON `gorm:"type:json"` + Private bool `gorm:"not null"` + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go index 0c07d87..deedd17 100644 --- a/backend/internal/models/user.go +++ b/backend/internal/models/user.go @@ -1,13 +1,13 @@ -package models - -import "time" - -type User struct { - ID string `gorm:"primaryKey"` - Username string `gorm:"not null"` - Password string `gorm:"not null"` - IsAdmin bool `gorm:"not null"` - IsActive bool `gorm:"not null"` - CreatedAt time.Time - UpdatedAt time.Time -} +package models + +import "time" + +type User struct { + ID string `gorm:"primaryKey"` + Username string `gorm:"not null"` + Password string `gorm:"not null"` + IsAdmin bool `gorm:"not null"` + IsActive bool `gorm:"not null"` + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/backend/internal/routes/album.go b/backend/internal/routes/album.go index 76b7069..cdeb789 100644 --- a/backend/internal/routes/album.go +++ b/backend/internal/routes/album.go @@ -1,20 +1,20 @@ -package routes - -import ( - "net/http" - - "github.com/gin-gonic/gin" - "github.com/wisplite/raster/internal/services" -) - -func RegisterAlbumRoutes(rg *gin.RouterGroup) { - album := rg.Group("/albums") - album.GET("/getPublic", func(c *gin.Context) { - albums, err := services.GetPublicAlbums() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, albums) - }) -} +package routes + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/wisplite/raster/internal/services" +) + +func RegisterAlbumRoutes(rg *gin.RouterGroup) { + album := rg.Group("/albums") + album.GET("/getPublic", func(c *gin.Context) { + albums, err := services.GetPublicAlbums() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, albums) + }) +} diff --git a/backend/internal/routes/routes.go b/backend/internal/routes/routes.go index 198ab4e..4b3eff9 100644 --- a/backend/internal/routes/routes.go +++ b/backend/internal/routes/routes.go @@ -1,8 +1,8 @@ -package routes - -import "github.com/gin-gonic/gin" - -func RegisterRoutes(r *gin.Engine) { - rg := r.Group("/api") - RegisterAlbumRoutes(rg) -} +package routes + +import "github.com/gin-gonic/gin" + +func RegisterRoutes(r *gin.Engine) { + rg := r.Group("/api") + RegisterAlbumRoutes(rg) +} diff --git a/backend/internal/routes/user.go b/backend/internal/routes/user.go index a10b74d..09fd34a 100644 --- a/backend/internal/routes/user.go +++ b/backend/internal/routes/user.go @@ -1,32 +1,32 @@ -package routes - -import ( - "net/http" - - "github.com/gin-gonic/gin" - "github.com/wisplite/raster/internal/services" -) - -func RegisterUserRoutes(rg *gin.RouterGroup) { - user := rg.Group("/user") - user.POST("/createUser", func(c *gin.Context) { - username := c.PostForm("username") - password := c.PostForm("password") - err := services.CreateUser(username, password) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{"message": "User created successfully"}) - }) - user.POST("/login", func(c *gin.Context) { - username := c.PostForm("username") - password := c.PostForm("password") - accessToken, err := services.Login(username, password) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - c.JSON(http.StatusOK, gin.H{"message": "Login successful", "accessToken": accessToken.Token}) - }) -} +package routes + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/wisplite/raster/internal/services" +) + +func RegisterUserRoutes(rg *gin.RouterGroup) { + user := rg.Group("/user") + user.POST("/createUser", func(c *gin.Context) { + username := c.PostForm("username") + password := c.PostForm("password") + err := services.CreateUser(username, password) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "User created successfully"}) + }) + user.POST("/login", func(c *gin.Context) { + username := c.PostForm("username") + password := c.PostForm("password") + accessToken, err := services.Login(username, password) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Login successful", "accessToken": accessToken.Token}) + }) +} diff --git a/backend/internal/services/accessTokens.go b/backend/internal/services/accessTokens.go index 1e9c508..55a546e 100644 --- a/backend/internal/services/accessTokens.go +++ b/backend/internal/services/accessTokens.go @@ -1,24 +1,24 @@ -package services - -import ( - "time" - - "github.com/google/uuid" - "github.com/wisplite/raster/internal/db" - "github.com/wisplite/raster/internal/models" -) - -func CreateAccessToken(userID string) (models.AccessToken, error) { - token := uuid.New().String() - expires := time.Now().Add(time.Hour * 24 * 30) - accessToken := models.AccessToken{ - Token: token, - UserID: userID, - Expires: expires, - } - result := db.GetDB().Create(&accessToken) - if result.Error != nil { - return models.AccessToken{}, result.Error - } - return accessToken, nil -} +package services + +import ( + "time" + + "github.com/google/uuid" + "github.com/wisplite/raster/internal/db" + "github.com/wisplite/raster/internal/models" +) + +func CreateAccessToken(userID string) (models.AccessToken, error) { + token := uuid.New().String() + expires := time.Now().Add(time.Hour * 24 * 30) + accessToken := models.AccessToken{ + Token: token, + UserID: userID, + Expires: expires, + } + result := db.GetDB().Create(&accessToken) + if result.Error != nil { + return models.AccessToken{}, result.Error + } + return accessToken, nil +} diff --git a/backend/internal/services/album.go b/backend/internal/services/album.go index 6788279..85590d8 100644 --- a/backend/internal/services/album.go +++ b/backend/internal/services/album.go @@ -1,25 +1,25 @@ -package services - -import ( - "github.com/wisplite/raster/internal/db" - "github.com/wisplite/raster/internal/models" -) - -func GetPublicAlbums() ([]models.Album, error) { - albums := []models.Album{} - result := db.GetDB().Where("private = ?", false).Find(&albums) - if result.Error != nil { - return []models.Album{}, result.Error - } - return albums, nil -} - -func GetAlbum(id string, authToken string) (models.Album, error) { - // TODO: Add authentication - album := models.Album{} - result := db.GetDB().First(&album, "id = ?", id) - if result.Error != nil { - return models.Album{}, result.Error - } - return album, nil -} +package services + +import ( + "github.com/wisplite/raster/internal/db" + "github.com/wisplite/raster/internal/models" +) + +func GetPublicAlbums() ([]models.Album, error) { + albums := []models.Album{} + result := db.GetDB().Where("private = ?", false).Find(&albums) + if result.Error != nil { + return []models.Album{}, result.Error + } + return albums, nil +} + +func GetAlbum(id string, authToken string) (models.Album, error) { + // TODO: Add authentication + album := models.Album{} + result := db.GetDB().First(&album, "id = ?", id) + if result.Error != nil { + return models.Album{}, result.Error + } + return album, nil +} diff --git a/backend/internal/services/user.go b/backend/internal/services/user.go index 161b268..d64fbba 100644 --- a/backend/internal/services/user.go +++ b/backend/internal/services/user.go @@ -1,45 +1,45 @@ -package services - -import ( - "log" - - "github.com/wisplite/raster/internal/db" - "github.com/wisplite/raster/internal/models" - "golang.org/x/crypto/bcrypt" -) - -func CreateUser(username string, password string) error { - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - log.Fatal("failed to hash password: ", err) - return err - } - user := models.User{ - Username: username, - Password: string(hashedPassword), - IsAdmin: false, - IsActive: false, - } - result := db.GetDB().Create(&user) - if result.Error != nil { - return result.Error - } - return nil -} - -func Login(username string, password string) (models.AccessToken, error) { - user := models.User{} - result := db.GetDB().First(&user, "username = ?", username) - if result.Error != nil { - return models.AccessToken{}, result.Error - } - err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) - if err != nil { - return models.AccessToken{}, err - } - accessToken, err := CreateAccessToken(user.ID) - if err != nil { - return models.AccessToken{}, err - } - return accessToken, nil -} +package services + +import ( + "log" + + "github.com/wisplite/raster/internal/db" + "github.com/wisplite/raster/internal/models" + "golang.org/x/crypto/bcrypt" +) + +func CreateUser(username string, password string) error { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + log.Fatal("failed to hash password: ", err) + return err + } + user := models.User{ + Username: username, + Password: string(hashedPassword), + IsAdmin: false, + IsActive: false, + } + result := db.GetDB().Create(&user) + if result.Error != nil { + return result.Error + } + return nil +} + +func Login(username string, password string) (models.AccessToken, error) { + user := models.User{} + result := db.GetDB().First(&user, "username = ?", username) + if result.Error != nil { + return models.AccessToken{}, result.Error + } + err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) + if err != nil { + return models.AccessToken{}, err + } + accessToken, err := CreateAccessToken(user.ID) + if err != nil { + return models.AccessToken{}, err + } + return accessToken, nil +}