Initial commit
This commit is contained in:
commit
c578b91c2d
46 files changed
+3241
No files matched your search
@@ -0,0 +1,16 @@
|
||||
package user
|
||||
|
||||
type User struct {
|
||||
Id int64
|
||||
Login string
|
||||
Password string
|
||||
DisplayName *string
|
||||
SuperUser bool
|
||||
}
|
||||
|
||||
func (u *User) EffectiveDisplayName() string {
|
||||
if u.DisplayName == nil {
|
||||
return u.Login
|
||||
}
|
||||
return *u.DisplayName
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
Db *sql.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *sql.DB) (*Repository, error) {
|
||||
return &Repository{Db: db}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateUser(ctx context.Context, login, password string) (*User, error) {
|
||||
query := "INSERT INTO users(login, password) VALUES(?, ?) RETURNING id, displayname, superuser"
|
||||
|
||||
user := &User{Login: login, Password: password}
|
||||
if err := r.Db.QueryRowContext(ctx, query, login, password).Scan(&user.Id, &user.DisplayName, &user.SuperUser); err != nil {
|
||||
return nil, fmt.Errorf("db insert failed: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetUsers(ctx context.Context, limit, offset int64) ([]User, error) {
|
||||
query := "SELECT id, login, password, displayname, superuser FROM users LIMIT ? OFFSET ?"
|
||||
rows, err := r.Db.QueryContext(ctx, query, limit, offset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("db select failed: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []User
|
||||
for rows.Next() {
|
||||
var user User
|
||||
|
||||
if err := rows.Scan(&user.Id, &user.Login, &user.Password, &user.DisplayName, &user.SuperUser); err != nil {
|
||||
return nil, fmt.Errorf("db select failed: %w", err)
|
||||
}
|
||||
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("db select failed: %w", err)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetUserCount(ctx context.Context) (int64, error) {
|
||||
query := "SELECT COUNT(*) FROM users"
|
||||
var result int64
|
||||
if err := r.Db.QueryRowContext(ctx, query).Scan(&result); err != nil {
|
||||
return 0, fmt.Errorf("db select failed: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetUserById(ctx context.Context, id int64) (*User, error) {
|
||||
query := "SELECT login, password, displayname, superuser FROM users WHERE id = ?"
|
||||
user := &User{Id: id}
|
||||
if err := r.Db.QueryRowContext(ctx, query, id).Scan(&user.Login, &user.Password, &user.DisplayName, &user.SuperUser); err != nil {
|
||||
return nil, fmt.Errorf("db select failed: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetUserByLogin(ctx context.Context, login string) (*User, error) {
|
||||
query := "SELECT id, password, displayname, superuser FROM users WHERE login = ?"
|
||||
user := &User{Login: login}
|
||||
if err := r.Db.QueryRowContext(ctx, query, login).Scan(&user.Id, &user.Password, &user.DisplayName, &user.SuperUser); err != nil {
|
||||
return nil, fmt.Errorf("db select failed: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateUser(ctx context.Context, user *User) error {
|
||||
query := "UPDATE users SET password = ?, displayname = ?, superuser = ? WHERE id = ?"
|
||||
|
||||
_, err := r.Db.ExecContext(ctx, query, user.Password, user.DisplayName, user.SuperUser, user.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("db update failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteUser(ctx context.Context, id int64) error {
|
||||
query := "DELETE FROM users WHERE id = ?"
|
||||
|
||||
_, err := r.Db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("db delete failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateMetadata(ctx context.Context, id int64, data map[string]any) error {
|
||||
query := "INSERT INTO metadatas(id, data) VALUES(?, ?)"
|
||||
|
||||
raw, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
_, err = r.Db.ExecContext(ctx, query, id, raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("db insert failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetMetadata(ctx context.Context, id int64) (map[string]any, error) {
|
||||
query := "SELECT data FROM metadatas WHERE id = ?"
|
||||
|
||||
var rawMetadata []byte
|
||||
if err := r.Db.QueryRowContext(ctx, query, id).Scan(&rawMetadata); err != nil {
|
||||
return nil, fmt.Errorf("db select failed: %w", err)
|
||||
}
|
||||
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(rawMetadata, &result); err != nil {
|
||||
return nil, fmt.Errorf("can't parse json: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateMetadata(ctx context.Context, id int64, data map[string]any) error {
|
||||
query := "UPDATE metadatas SET data = ? WHERE id = ?"
|
||||
|
||||
raw, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
_, err = r.Db.ExecContext(ctx, query, raw, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("db update failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteMetadata(ctx context.Context, id int64) error {
|
||||
query := "DELETE FROM metadatas WHERE id = ?"
|
||||
|
||||
_, err := r.Db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("db delete failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) CreateAvatar(ctx context.Context, id int64, data []byte) error {
|
||||
query := "INSERT INTO avatars(id, data) VALUES(?, ?)"
|
||||
|
||||
_, err := r.Db.ExecContext(ctx, query, id, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("db insert failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetAvatar(ctx context.Context, id int64) ([]byte, error) {
|
||||
query := "SELECT data FROM avatars WHERE id = ?"
|
||||
|
||||
var result []byte
|
||||
if err := r.Db.QueryRowContext(ctx, query, id).Scan(&result); err != nil {
|
||||
return nil, fmt.Errorf("db select failed: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateAvatar(ctx context.Context, id int64, data []byte) error {
|
||||
query := "UPDATE avatars SET data = ? WHERE id = ?"
|
||||
|
||||
_, err := r.Db.ExecContext(ctx, query, data, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("db update failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) DeleteAvatar(ctx context.Context, id int64) error {
|
||||
query := "DELETE FROM avatars WHERE id = ?"
|
||||
|
||||
_, err := r.Db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("db delete failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxWidth = 512
|
||||
MaxHeight = 512
|
||||
MaxUploadSize = 5 * 1024 * 1024
|
||||
)
|
||||
|
||||
func VerifyImageLimit(r io.Reader) error {
|
||||
cfg, format, err := image.DecodeConfig(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode image: %w", err)
|
||||
}
|
||||
|
||||
switch format {
|
||||
case "jpeg", "png":
|
||||
// Allowed formats
|
||||
default:
|
||||
return fmt.Errorf("not supported format: %s", format)
|
||||
}
|
||||
|
||||
if cfg.Width > MaxWidth || cfg.Height > MaxHeight {
|
||||
return fmt.Errorf("image dimensions exceed limits: %dx%d (max %dx%d)", cfg.Width, cfg.Height, MaxWidth, MaxHeight)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExtractUploadedImageBytes(r *http.Request, formKey string) ([]byte, string, error) {
|
||||
// Read file with a size limit to prevent abuse. We read the raw multipart
|
||||
// bytes, verify size and image format, then return them.
|
||||
const maxRead = MaxUploadSize + 1024 // small extra for multipart framing
|
||||
|
||||
file, header, err := r.FormFile(formKey)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("form error: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" {
|
||||
return nil, "", fmt.Errorf("invalid file type: only JPG or PNG are allowed")
|
||||
}
|
||||
|
||||
// Read all bytes so we can verify image dimensions from a reusable reader.
|
||||
lr := io.LimitReader(file, maxRead)
|
||||
fileBytes, err := io.ReadAll(lr)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if int64(len(fileBytes)) >= maxRead {
|
||||
return nil, "", fmt.Errorf("file too large (max %d bytes)", MaxUploadSize)
|
||||
}
|
||||
|
||||
if err := VerifyImageLimit(bytes.NewReader(fileBytes)); err != nil {
|
||||
return nil, "", fmt.Errorf("invalid file: %w", err)
|
||||
}
|
||||
|
||||
return fileBytes, header.Filename, nil
|
||||
}
|
||||
Reference in new issue
Block a user