diff --git a/backend/migrations/007_add_scan_settings.up.sql b/backend/migrations/007_add_scan_settings.up.sql new file mode 100644 index 0000000..1b3b7e4 --- /dev/null +++ b/backend/migrations/007_add_scan_settings.up.sql @@ -0,0 +1,3 @@ +-- Add scan settings to users table +ALTER TABLE users ADD COLUMN scan_frequency_minutes INTEGER DEFAULT 60; +ALTER TABLE users ADD COLUMN auto_scan_enabled BOOLEAN DEFAULT true; \ No newline at end of file diff --git a/bruno/library/Get Scan Settings.bru b/bruno/library/Get Scan Settings.bru new file mode 100644 index 0000000..0f48cbf --- /dev/null +++ b/bruno/library/Get Scan Settings.bru @@ -0,0 +1,36 @@ +meta { + name: Get Scan Settings + type: http + seq: 2 +} + +get { + url: {{base_url}}/api/library/scan-settings + body: none + auth: inherit +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Get Scan Settings + + Retrieves the user's current ebook scanning settings. + + **Method:** GET + + **Endpoint:** /api/library/scan-settings + + **Authentication:** Required + + **Response:** + - `scan_frequency_minutes` (integer): Minutes between automatic scans + - `auto_scan_enabled` (boolean): Whether automatic scanning is enabled + + **Status Codes:** + - 200: Success + - 401: Unauthorized +} \ No newline at end of file diff --git a/bruno/library/Update Scan Settings.bru b/bruno/library/Update Scan Settings.bru new file mode 100644 index 0000000..9d62f1d --- /dev/null +++ b/bruno/library/Update Scan Settings.bru @@ -0,0 +1,47 @@ +meta { + name: Update Scan Settings + type: http + seq: 1 +} + +put { + url: {{base_url}}/api/library/scan-settings + body: json + auth: inherit +} + +body { + { + "scan_frequency_minutes": 60, + "auto_scan_enabled": true + } +} + +settings { + encodeUrl: true + timeout: 0 +} + +docs { + ## Update Scan Settings + + Updates the user's ebook scanning settings. + + **Method:** PUT + + **Endpoint:** /api/library/scan-settings + + **Authentication:** Required + + **Request Body:** + - `scan_frequency_minutes` (integer, required): Minutes between automatic scans (15-1440) + - `auto_scan_enabled` (boolean, required): Whether automatic scanning is enabled + + **Response:** + - `message` (string): Success message + + **Status Codes:** + - 200: Success + - 400: Invalid settings + - 401: Unauthorized +} \ No newline at end of file diff --git a/internal/database/models.go b/internal/database/models.go index 4513cf7..ca89528 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -55,11 +55,13 @@ type UserEbookFolders struct { } type Users struct { - ID pgtype.UUID `db:"id" json:"id"` - Email string `db:"email" json:"email"` - Username string `db:"username" json:"username"` - PasswordHash string `db:"password_hash" json:"password_hash"` - Theme pgtype.Text `db:"theme" json:"theme"` - CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` - UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + ID pgtype.UUID `db:"id" json:"id"` + Email string `db:"email" json:"email"` + Username string `db:"username" json:"username"` + PasswordHash string `db:"password_hash" json:"password_hash"` + Theme pgtype.Text `db:"theme" json:"theme"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + ScanFrequencyMinutes pgtype.Int4 `db:"scan_frequency_minutes" json:"scan_frequency_minutes"` + AutoScanEnabled pgtype.Bool `db:"auto_scan_enabled" json:"auto_scan_enabled"` } diff --git a/internal/database/querier.go b/internal/database/querier.go index 400cd1e..b1bb740 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -25,6 +25,7 @@ type Querier interface { GetEbookRating(ctx context.Context, arg GetEbookRatingParams) (EbookRatings, error) GetEbookRatings(ctx context.Context, ebookID pgtype.UUID) ([]GetEbookRatingsRow, error) GetReadingProgress(ctx context.Context, arg GetReadingProgressParams) (ReadingProgress, error) + GetScanSettings(ctx context.Context, id pgtype.UUID) (GetScanSettingsRow, error) GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, error) GetUserByEmail(ctx context.Context, email string) (Users, error) GetUserByEmailOrUsername(ctx context.Context, email string) (Users, error) @@ -38,6 +39,7 @@ type Querier interface { UpdateEmail(ctx context.Context, arg UpdateEmailParams) error UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error) + UpdateScanSettings(ctx context.Context, arg UpdateScanSettingsParams) error UpdateUserTheme(ctx context.Context, arg UpdateUserThemeParams) error UpdateUsername(ctx context.Context, arg UpdateUsernameParams) error } diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index b231098..7789f21 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -131,7 +131,7 @@ func (q *Queries) CreateEbookRating(ctx context.Context, arg CreateEbookRatingPa const CreateUser = `-- name: CreateUser :one INSERT INTO users (email, username, password_hash, theme) VALUES ($1, $2, $3, $4) -RETURNING id, email, username, password_hash, theme, created_at, updated_at +RETURNING id, email, username, password_hash, theme, created_at, updated_at, scan_frequency_minutes, auto_scan_enabled ` type CreateUserParams struct { @@ -157,6 +157,8 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (Users, &i.Theme, &i.CreatedAt, &i.UpdatedAt, + &i.ScanFrequencyMinutes, + &i.AutoScanEnabled, ) return i, err } @@ -373,6 +375,22 @@ func (q *Queries) GetReadingProgress(ctx context.Context, arg GetReadingProgress return i, err } +const GetScanSettings = `-- name: GetScanSettings :one +SELECT scan_frequency_minutes, auto_scan_enabled FROM users WHERE id = $1 +` + +type GetScanSettingsRow struct { + ScanFrequencyMinutes pgtype.Int4 `db:"scan_frequency_minutes" json:"scan_frequency_minutes"` + AutoScanEnabled pgtype.Bool `db:"auto_scan_enabled" json:"auto_scan_enabled"` +} + +func (q *Queries) GetScanSettings(ctx context.Context, id pgtype.UUID) (GetScanSettingsRow, error) { + row := q.db.QueryRow(ctx, GetScanSettings, id) + var i GetScanSettingsRow + err := row.Scan(&i.ScanFrequencyMinutes, &i.AutoScanEnabled) + return i, err +} + const GetUser = `-- name: GetUser :one SELECT id, email, username, theme, created_at, updated_at FROM users WHERE id = $1 ` @@ -401,7 +419,7 @@ func (q *Queries) GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, erro } const GetUserByEmail = `-- name: GetUserByEmail :one -SELECT id, email, username, password_hash, theme, created_at, updated_at FROM users WHERE email = $1 +SELECT id, email, username, password_hash, theme, created_at, updated_at, scan_frequency_minutes, auto_scan_enabled FROM users WHERE email = $1 ` func (q *Queries) GetUserByEmail(ctx context.Context, email string) (Users, error) { @@ -415,12 +433,14 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (Users, erro &i.Theme, &i.CreatedAt, &i.UpdatedAt, + &i.ScanFrequencyMinutes, + &i.AutoScanEnabled, ) return i, err } const GetUserByEmailOrUsername = `-- name: GetUserByEmailOrUsername :one -SELECT id, email, username, password_hash, theme, created_at, updated_at FROM users WHERE email = $1 OR username = $1 +SELECT id, email, username, password_hash, theme, created_at, updated_at, scan_frequency_minutes, auto_scan_enabled FROM users WHERE email = $1 OR username = $1 ` func (q *Queries) GetUserByEmailOrUsername(ctx context.Context, email string) (Users, error) { @@ -434,12 +454,14 @@ func (q *Queries) GetUserByEmailOrUsername(ctx context.Context, email string) (U &i.Theme, &i.CreatedAt, &i.UpdatedAt, + &i.ScanFrequencyMinutes, + &i.AutoScanEnabled, ) return i, err } const GetUserByUsername = `-- name: GetUserByUsername :one -SELECT id, email, username, password_hash, theme, created_at, updated_at FROM users WHERE username = $1 +SELECT id, email, username, password_hash, theme, created_at, updated_at, scan_frequency_minutes, auto_scan_enabled FROM users WHERE username = $1 ` func (q *Queries) GetUserByUsername(ctx context.Context, username string) (Users, error) { @@ -453,6 +475,8 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (Users &i.Theme, &i.CreatedAt, &i.UpdatedAt, + &i.ScanFrequencyMinutes, + &i.AutoScanEnabled, ) return i, err } @@ -753,6 +777,21 @@ func (q *Queries) UpdateReadingProgress(ctx context.Context, arg UpdateReadingPr return i, err } +const UpdateScanSettings = `-- name: UpdateScanSettings :exec +UPDATE users SET scan_frequency_minutes = $2, auto_scan_enabled = $3, updated_at = NOW() WHERE id = $1 +` + +type UpdateScanSettingsParams struct { + ID pgtype.UUID `db:"id" json:"id"` + ScanFrequencyMinutes pgtype.Int4 `db:"scan_frequency_minutes" json:"scan_frequency_minutes"` + AutoScanEnabled pgtype.Bool `db:"auto_scan_enabled" json:"auto_scan_enabled"` +} + +func (q *Queries) UpdateScanSettings(ctx context.Context, arg UpdateScanSettingsParams) error { + _, err := q.db.Exec(ctx, UpdateScanSettings, arg.ID, arg.ScanFrequencyMinutes, arg.AutoScanEnabled) + return err +} + const UpdateUserTheme = `-- name: UpdateUserTheme :exec UPDATE users SET theme = $2, updated_at = NOW() WHERE id = $1 ` diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 1112ed4..212639a 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -84,6 +84,12 @@ UPDATE users SET password_hash = $2, updated_at = NOW() WHERE id = $1; -- name: DeleteUser :exec DELETE FROM users WHERE id = $1; +-- name: UpdateScanSettings :exec +UPDATE users SET scan_frequency_minutes = $2, auto_scan_enabled = $3, updated_at = NOW() WHERE id = $1; + +-- name: GetScanSettings :one +SELECT scan_frequency_minutes, auto_scan_enabled FROM users WHERE id = $1; + -- name: CreateEbookRating :one INSERT INTO ebook_ratings (ebook_id, user_id, rating) VALUES ($1, $2, $3) diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index 635c02c..f324ba1 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -588,6 +588,62 @@ func (h *AuthHandler) DeleteAccount(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"message": "account deleted successfully"}) } +type UpdateScanSettingsRequest struct { + ScanFrequencyMinutes int32 `json:"scan_frequency_minutes" validate:"required,min=15,max=1440"` + AutoScanEnabled bool `json:"auto_scan_enabled"` +} + +// UpdateScanSettings handles PUT /api/library/scan-settings +func (h *AuthHandler) UpdateScanSettings(c echo.Context) error { + userID := c.Get("user_id").(string) + userUUID, err := uuid.Parse(userID) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) + } + + var req UpdateScanSettingsRequest + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) + } + if err := c.Validate(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) + } + + err = h.db.UpdateScanSettings(c.Request().Context(), database.UpdateScanSettingsParams{ + ID: pgtype.UUID{Bytes: userUUID, Valid: true}, + ScanFrequencyMinutes: pgtype.Int4{Int32: req.ScanFrequencyMinutes, Valid: true}, + AutoScanEnabled: pgtype.Bool{Bool: req.AutoScanEnabled, Valid: true}, + }) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) + } + + return c.JSON(http.StatusOK, map[string]string{"message": "scan settings updated successfully"}) +} + +// GetScanSettings handles GET /api/library/scan-settings +func (h *AuthHandler) GetScanSettings(c echo.Context) error { + userID := c.Get("user_id").(string) + userUUID, err := uuid.Parse(userID) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) + } + + settings, err := h.db.GetScanSettings(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true}) + if err != nil { + // If no settings found, return defaults + return c.JSON(http.StatusOK, map[string]interface{}{ + "scan_frequency_minutes": 60, + "auto_scan_enabled": true, + }) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "scan_frequency_minutes": settings.ScanFrequencyMinutes.Int32, + "auto_scan_enabled": settings.AutoScanEnabled.Bool, + }) +} + func (h *AuthHandler) generateJWT(userID string) (string, error) { claims := jwtgo.MapClaims{ "user_id": userID, diff --git a/migrations/007_add_scan_settings.up.sql b/migrations/007_add_scan_settings.up.sql new file mode 100644 index 0000000..1b3b7e4 --- /dev/null +++ b/migrations/007_add_scan_settings.up.sql @@ -0,0 +1,3 @@ +-- Add scan settings to users table +ALTER TABLE users ADD COLUMN scan_frequency_minutes INTEGER DEFAULT 60; +ALTER TABLE users ADD COLUMN auto_scan_enabled BOOLEAN DEFAULT true; \ No newline at end of file diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..6ea7df3 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,345 @@ +{{template "base.html" .}} + +{{define "title"}}Admin Dashboard - Bookmann{{end}} + +{{define "content"}} + + + +
Manage your account settings and library preferences
+Manage the folders where Bookmann scans for ebooks
+ + +Automatically scan folders for new ebooks
+Manually scan all configured folders
++ Once you delete your account, there is no going back. Please be certain. +
+ +