diff --git a/backend/internal/models/album.go b/backend/internal/models/album.go index 1449f92..bffcc17 100644 --- a/backend/internal/models/album.go +++ b/backend/internal/models/album.go @@ -14,8 +14,8 @@ type Album struct { Private bool `gorm:"not null"` // Public albums have a default access level of 0 for all visitors, including guests. // Private albums require a user with access to be logged in to view, or a magic link to be used. - ParentID string `gorm:"not null"` // The ID of the parent album, if any. This is an empty string for root albums. - Thumbnail string `gorm:"not null"` // The media ID of the thumbnail for the album. + ParentID string `gorm:"not null"` // The ID of the parent album, if any. This is an empty string for root albums. + Thumbnail string `gorm:"not null;default:''"` // The media ID of the thumbnail for the album. CreatedAt time.Time UpdatedAt time.Time } diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go index b707f1a..e1e3dff 100644 --- a/backend/internal/models/user.go +++ b/backend/internal/models/user.go @@ -4,7 +4,7 @@ import "time" type User struct { ID string `gorm:"primaryKey"` - Username string `gorm:"not null"` + Username string `gorm:"not null unique"` Password string `gorm:"not null"` IsAdmin bool `gorm:"not null"` IsRoot bool `gorm:"not null"` diff --git a/backend/internal/routes/album.go b/backend/internal/routes/album.go index 4374b29..f560f4b 100644 --- a/backend/internal/routes/album.go +++ b/backend/internal/routes/album.go @@ -51,4 +51,19 @@ func RegisterAlbumRoutes(rg *gin.RouterGroup) { } c.JSON(http.StatusOK, result) }) + album.POST("/getIDFromPath", func(c *gin.Context) { + var request struct { + Path string `json:"path"` + } + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + id, err := services.GetIDFromPath(request.Path) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Album not found"}) + return + } + c.JSON(http.StatusOK, gin.H{"id": id}) + }) } diff --git a/backend/internal/services/album.go b/backend/internal/services/album.go index 2cbf016..fca1c47 100644 --- a/backend/internal/services/album.go +++ b/backend/internal/services/album.go @@ -2,6 +2,8 @@ package services import ( "fmt" + "regexp" + "strings" "github.com/google/uuid" "github.com/wisplite/raster/internal/db" @@ -67,19 +69,31 @@ func CreateAlbum(accessToken string, title string, description string, parentID if accessLevel < 2 { return models.Album{}, fmt.Errorf("user does not have permission to create albums in this parent") } + if !regexp.MustCompile(`^[a-zA-Z0-9\s\-_]+$`).MatchString(title) { + return models.Album{}, fmt.Errorf("title can only contain alphanumeric characters, spaces, and hyphens/underscores") + } + // check for duplicate title in parent + existingAlbum := models.Album{} + result := db.GetDB().First(&existingAlbum, "title = ? AND parent_id = ?", title, parentID) + if result.Error != nil && result.Error != gorm.ErrRecordNotFound { + return models.Album{}, result.Error + } + if existingAlbum.ID != "" { + return models.Album{}, fmt.Errorf("album with this title already exists in this parent") + } albumID := uuid.New().String() - album := models.Album{ + newAlbum := models.Album{ ID: albumID, Title: title, Description: description, ParentID: parentID, Thumbnail: "", } - result := db.GetDB().Create(&album) - if result.Error != nil { + newAlbumResult := db.GetDB().Create(&newAlbum) + if newAlbumResult.Error != nil { return models.Album{}, result.Error } - return album, nil + return newAlbum, nil } func CheckUserAlbumAccess(userID string, albumID string) (int, error) { @@ -100,3 +114,23 @@ func CheckUserAlbumAccess(userID string, albumID string) (int, error) { } return userAccess.AccessLevel, nil } + +func GetIDFromPath(path string) (string, error) { + currentParentID := "" + segments := strings.Split(path, "/") + + for _, segment := range segments { + if segment == "" { + continue + } + + var album models.Album + result := db.GetDB().Where("title = ? AND parent_id = ?", segment, currentParentID).First(&album) + if result.Error != nil { + return "", result.Error + } + currentParentID = album.ID + } + + return currentParentID, nil +} diff --git a/frontend/src/account/createRoot.jsx b/frontend/src/account/createRoot.jsx index 48ff233..b4890cc 100644 --- a/frontend/src/account/createRoot.jsx +++ b/frontend/src/account/createRoot.jsx @@ -1,9 +1,11 @@ import { useState } from 'react' import { getServerUrl } from '../hooks/getConstants' import { useNavigate } from 'react-router-dom' +import { useNotifier } from '../contexts/useNotifier' export default function CreateRootUser() { const navigate = useNavigate() + const { showError, showSuccess } = useNotifier() const [username, setUsername] = useState('') const [password, setPassword] = useState('') const handleCreateRootUser = async () => { @@ -19,7 +21,7 @@ export default function CreateRootUser() { }) const data = await response.json() if (data.error) { - console.error(data.error) + showError(data.error) } else { const rootResponse = await fetch(`${getServerUrl()}/api/user/setRootUser`, { method: 'POST', @@ -32,9 +34,10 @@ export default function CreateRootUser() { }) const rootData = await rootResponse.json() if (rootData.error) { - console.error(rootData.error) + showError(rootData.error) } else { navigate('/gallery') + showSuccess('Root user created successfully') } } } diff --git a/frontend/src/components/NavBar.jsx b/frontend/src/components/NavBar.jsx index 3078ab0..7da4522 100644 --- a/frontend/src/components/NavBar.jsx +++ b/frontend/src/components/NavBar.jsx @@ -12,7 +12,7 @@ export default function NavBar({ path }) { {path.map((item, index) => (
/
}{album.Title}