Compare commits
8 Commits
ml2
...
dd2960a742
| Author | SHA1 | Date | |
|---|---|---|---|
| dd2960a742 | |||
| c7a440e38f | |||
| d12162fc3b | |||
| 711b1ad5d1 | |||
| af69c4fe07 | |||
| 5eb90b18d5 | |||
| bd90fb339f | |||
| e4c879db36 |
@@ -52,6 +52,7 @@ require (
|
||||
github.com/minio/crc64nvme v1.0.2 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.95 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
|
||||
@@ -99,6 +99,8 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
|
||||
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
||||
@@ -88,6 +88,7 @@ type Wish struct {
|
||||
ID int64
|
||||
Guid pgtype.UUID
|
||||
WishListID int64
|
||||
WishListGuid pgtype.UUID
|
||||
Name string
|
||||
Description string
|
||||
PictureUrl string
|
||||
@@ -104,8 +105,8 @@ type WishList struct {
|
||||
ProfileID int64
|
||||
Hidden bool
|
||||
Name string
|
||||
IconName *string
|
||||
Color *string
|
||||
ColorGrad *string
|
||||
IconName string
|
||||
Color string
|
||||
ColorGrad string
|
||||
Deleted bool
|
||||
}
|
||||
|
||||
@@ -11,6 +11,52 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const checkProfileAccess = `-- name: CheckProfileAccess :one
|
||||
SELECT
|
||||
CASE WHEN u.deleted OR NOT u.verified THEN TRUE ELSE FALSE END AS user_unavailable,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM banned_users
|
||||
WHERE user_id = u.id AND
|
||||
pardoned IS FALSE AND
|
||||
(expires_at IS NULL OR expires_at < CURRENT_TIMESTAMP)
|
||||
) THEN TRUE ELSE FALSE END AS user_banned,
|
||||
CASE WHEN ps.hide_profile_details THEN TRUE ELSE FALSE END AS hidden,
|
||||
CASE WHEN ps.hide_for_unauthenticated AND $2::text = '' THEN TRUE ELSE FALSE END AS auth_required,
|
||||
CASE WHEN ps.captcha THEN TRUE ELSE FALSE END AS captcha_required
|
||||
FROM profiles p
|
||||
JOIN profile_settings ps ON ps.profile_id = p.id
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.id = $1
|
||||
`
|
||||
|
||||
type CheckProfileAccessParams struct {
|
||||
ID int64
|
||||
Requester string
|
||||
}
|
||||
|
||||
type CheckProfileAccessRow struct {
|
||||
UserUnavailable bool
|
||||
UserBanned bool
|
||||
Hidden bool
|
||||
AuthRequired bool
|
||||
CaptchaRequired bool
|
||||
}
|
||||
|
||||
// XXX: recheck, was tired
|
||||
func (q *Queries) CheckProfileAccess(ctx context.Context, arg CheckProfileAccessParams) (CheckProfileAccessRow, error) {
|
||||
row := q.db.QueryRow(ctx, checkProfileAccess, arg.ID, arg.Requester)
|
||||
var i CheckProfileAccessRow
|
||||
err := row.Scan(
|
||||
&i.UserUnavailable,
|
||||
&i.UserBanned,
|
||||
&i.Hidden,
|
||||
&i.AuthRequired,
|
||||
&i.CaptchaRequired,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const checkUserRegistrationAvailability = `-- name: CheckUserRegistrationAvailability :one
|
||||
SELECT
|
||||
COUNT(CASE WHEN users.username = $1::text THEN 1 END) > 0 AS username_busy,
|
||||
@@ -253,6 +299,60 @@ func (q *Queries) CreateUser(ctx context.Context, username string) (User, error)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createWish = `-- name: CreateWish :one
|
||||
INSERT INTO wishes(
|
||||
wish_list_id,
|
||||
wish_list_guid,
|
||||
name,
|
||||
description,
|
||||
picture_url,
|
||||
stars)
|
||||
VALUES
|
||||
(
|
||||
(SELECT id FROM wish_lists wl WHERE wl.guid = ($1::text)::uuid),
|
||||
($1::text)::uuid,
|
||||
$2::text,
|
||||
$3::text,
|
||||
$4::text,
|
||||
$5::smallint
|
||||
)
|
||||
RETURNING id, guid, wish_list_id, wish_list_guid, name, description, picture_url, stars, creation_date, fulfilled, fulfilled_date, deleted
|
||||
`
|
||||
|
||||
type CreateWishParams struct {
|
||||
WishListGuid string
|
||||
Name string
|
||||
Description string
|
||||
PictureUrl string
|
||||
Stars int16
|
||||
}
|
||||
|
||||
func (q *Queries) CreateWish(ctx context.Context, arg CreateWishParams) (Wish, error) {
|
||||
row := q.db.QueryRow(ctx, createWish,
|
||||
arg.WishListGuid,
|
||||
arg.Name,
|
||||
arg.Description,
|
||||
arg.PictureUrl,
|
||||
arg.Stars,
|
||||
)
|
||||
var i Wish
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Guid,
|
||||
&i.WishListID,
|
||||
&i.WishListGuid,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.PictureUrl,
|
||||
&i.Stars,
|
||||
&i.CreationDate,
|
||||
&i.Fulfilled,
|
||||
&i.FulfilledDate,
|
||||
&i.Deleted,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createWishList = `-- name: CreateWishList :one
|
||||
INSERT INTO wish_lists(profile_id, hidden, name, icon_name, color, color_grad)
|
||||
VALUES (
|
||||
@@ -401,34 +501,15 @@ SELECT
|
||||
p.avatar_url,
|
||||
CASE WHEN ps.hide_birthday THEN NULL ELSE p.birthday END AS birthday,
|
||||
p.color,
|
||||
p.color_grad,
|
||||
NOT ($1::text = '' AND ps.hide_for_unauthenticated) AS access_allowed
|
||||
p.color_grad
|
||||
FROM
|
||||
users AS u
|
||||
JOIN profiles AS p ON u.id = p.user_id
|
||||
JOIN profile_settings AS ps ON p.id = ps.profile_id
|
||||
WHERE
|
||||
u.username = $2::text
|
||||
AND (
|
||||
$2::text = $1::text
|
||||
OR
|
||||
u.deleted IS FALSE
|
||||
AND u.verified IS TRUE
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM banned_users
|
||||
WHERE user_id = u.id AND
|
||||
pardoned IS FALSE AND
|
||||
(expires_at IS NULL OR expires_at < CURRENT_TIMESTAMP)
|
||||
)
|
||||
)
|
||||
u.username = $1::text
|
||||
`
|
||||
|
||||
type GetProfileByUsernameWithPrivacyParams struct {
|
||||
Requester string
|
||||
SearchedUsername string
|
||||
}
|
||||
|
||||
type GetProfileByUsernameWithPrivacyRow struct {
|
||||
Username string
|
||||
Name string
|
||||
@@ -437,11 +518,11 @@ type GetProfileByUsernameWithPrivacyRow struct {
|
||||
Birthday pgtype.Timestamp
|
||||
Color string
|
||||
ColorGrad string
|
||||
AccessAllowed *bool
|
||||
}
|
||||
|
||||
func (q *Queries) GetProfileByUsernameWithPrivacy(ctx context.Context, arg GetProfileByUsernameWithPrivacyParams) (GetProfileByUsernameWithPrivacyRow, error) {
|
||||
row := q.db.QueryRow(ctx, getProfileByUsernameWithPrivacy, arg.Requester, arg.SearchedUsername)
|
||||
// FIXME: tweak backend code to handle privacy correctly
|
||||
func (q *Queries) GetProfileByUsernameWithPrivacy(ctx context.Context, searchedUsername string) (GetProfileByUsernameWithPrivacyRow, error) {
|
||||
row := q.db.QueryRow(ctx, getProfileByUsernameWithPrivacy, searchedUsername)
|
||||
var i GetProfileByUsernameWithPrivacyRow
|
||||
err := row.Scan(
|
||||
&i.Username,
|
||||
@@ -451,7 +532,6 @@ func (q *Queries) GetProfileByUsernameWithPrivacy(ctx context.Context, arg GetPr
|
||||
&i.Birthday,
|
||||
&i.Color,
|
||||
&i.ColorGrad,
|
||||
&i.AccessAllowed,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -836,7 +916,7 @@ WHERE
|
||||
FROM banned_users
|
||||
WHERE user_id = users.id AND
|
||||
pardoned IS FALSE AND
|
||||
(expires_at IS NULL OR expires_at < CURRENT_TIMESTAMP)
|
||||
(expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
|
||||
) AND -- Not banned
|
||||
linfo.password_hash = crypt($2::text, linfo.password_hash)
|
||||
`
|
||||
@@ -911,6 +991,102 @@ func (q *Queries) GetValidUserSessions(ctx context.Context, userID int64) ([]Ses
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWishByGuid = `-- name: GetWishByGuid :one
|
||||
SELECT id, guid, wish_list_id, wish_list_guid, name, description, picture_url, stars, creation_date, fulfilled, fulfilled_date, deleted FROM wishes w
|
||||
WHERE w.guid = ($1::text)::uuid
|
||||
`
|
||||
|
||||
func (q *Queries) GetWishByGuid(ctx context.Context, guid string) (Wish, error) {
|
||||
row := q.db.QueryRow(ctx, getWishByGuid, guid)
|
||||
var i Wish
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Guid,
|
||||
&i.WishListID,
|
||||
&i.WishListGuid,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.PictureUrl,
|
||||
&i.Stars,
|
||||
&i.CreationDate,
|
||||
&i.Fulfilled,
|
||||
&i.FulfilledDate,
|
||||
&i.Deleted,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWishByGuidWithPrivacy = `-- name: GetWishByGuidWithPrivacy :one
|
||||
SELECT
|
||||
w.id, w.guid, w.wish_list_id, w.wish_list_guid, w.name, w.description, w.picture_url, w.stars, w.creation_date, w.fulfilled, w.fulfilled_date, w.deleted,
|
||||
CASE
|
||||
WHEN
|
||||
(
|
||||
$1::text = u.username OR
|
||||
NOT ps.hide_profile_details AND
|
||||
NOT
|
||||
(
|
||||
ps.hide_for_unauthenticated AND
|
||||
$1::text = ''
|
||||
) AND
|
||||
NOT wl.hidden
|
||||
)
|
||||
THEN TRUE
|
||||
ELSE FALSE
|
||||
END AS access_allowed
|
||||
FROM wishes w
|
||||
JOIN wish_lists wl ON w.wish_list_id = wl.id
|
||||
JOIN profiles p ON wl.profile_id = p.id
|
||||
JOIN profile_settings ps ON ps.profile_id = p.id
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE
|
||||
w.guid = ($2::text)::uuid AND
|
||||
w.deleted IS FALSE
|
||||
`
|
||||
|
||||
type GetWishByGuidWithPrivacyParams struct {
|
||||
Requester string
|
||||
Guid string
|
||||
}
|
||||
|
||||
type GetWishByGuidWithPrivacyRow struct {
|
||||
ID int64
|
||||
Guid pgtype.UUID
|
||||
WishListID int64
|
||||
WishListGuid pgtype.UUID
|
||||
Name string
|
||||
Description string
|
||||
PictureUrl string
|
||||
Stars int16
|
||||
CreationDate pgtype.Timestamp
|
||||
Fulfilled bool
|
||||
FulfilledDate pgtype.Timestamp
|
||||
Deleted bool
|
||||
AccessAllowed bool
|
||||
}
|
||||
|
||||
// XXX: Obsolete, use the according access check query instead
|
||||
func (q *Queries) GetWishByGuidWithPrivacy(ctx context.Context, arg GetWishByGuidWithPrivacyParams) (GetWishByGuidWithPrivacyRow, error) {
|
||||
row := q.db.QueryRow(ctx, getWishByGuidWithPrivacy, arg.Requester, arg.Guid)
|
||||
var i GetWishByGuidWithPrivacyRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Guid,
|
||||
&i.WishListID,
|
||||
&i.WishListGuid,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.PictureUrl,
|
||||
&i.Stars,
|
||||
&i.CreationDate,
|
||||
&i.Fulfilled,
|
||||
&i.FulfilledDate,
|
||||
&i.Deleted,
|
||||
&i.AccessAllowed,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWishlistByGuid = `-- name: GetWishlistByGuid :one
|
||||
SELECT id, guid, profile_id, hidden, name, icon_name, color, color_grad, deleted FROM wish_lists wl
|
||||
WHERE wl.guid = ($1::text)::uuid
|
||||
@@ -933,11 +1109,93 @@ func (q *Queries) GetWishlistByGuid(ctx context.Context, guid string) (WishList,
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getWishlistsByUsername = `-- name: GetWishlistsByUsername :many
|
||||
SELECT wl.id, guid, profile_id, hidden, wl.name, icon_name, wl.color, wl.color_grad, wl.deleted, p.id, user_id, p.name, bio, avatar_url, birthday, p.color, p.color_grad, u.id, username, verified, registration_date, role, u.deleted FROM wish_lists wl
|
||||
JOIN profiles p ON p.id = wl.profile_id
|
||||
JOIN users u ON u.id = p.user_id
|
||||
WHERE u.username = $1::text
|
||||
`
|
||||
|
||||
type GetWishlistsByUsernameRow struct {
|
||||
ID int64
|
||||
Guid pgtype.UUID
|
||||
ProfileID int64
|
||||
Hidden bool
|
||||
Name string
|
||||
IconName string
|
||||
Color string
|
||||
ColorGrad string
|
||||
Deleted bool
|
||||
ID_2 int64
|
||||
UserID int64
|
||||
Name_2 string
|
||||
Bio string
|
||||
AvatarUrl string
|
||||
Birthday pgtype.Timestamp
|
||||
Color_2 string
|
||||
ColorGrad_2 string
|
||||
ID_3 int64
|
||||
Username string
|
||||
Verified bool
|
||||
RegistrationDate pgtype.Timestamp
|
||||
Role int32
|
||||
Deleted_2 *bool
|
||||
}
|
||||
|
||||
func (q *Queries) GetWishlistsByUsername(ctx context.Context, username string) ([]GetWishlistsByUsernameRow, error) {
|
||||
rows, err := q.db.Query(ctx, getWishlistsByUsername, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetWishlistsByUsernameRow
|
||||
for rows.Next() {
|
||||
var i GetWishlistsByUsernameRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Guid,
|
||||
&i.ProfileID,
|
||||
&i.Hidden,
|
||||
&i.Name,
|
||||
&i.IconName,
|
||||
&i.Color,
|
||||
&i.ColorGrad,
|
||||
&i.Deleted,
|
||||
&i.ID_2,
|
||||
&i.UserID,
|
||||
&i.Name_2,
|
||||
&i.Bio,
|
||||
&i.AvatarUrl,
|
||||
&i.Birthday,
|
||||
&i.Color_2,
|
||||
&i.ColorGrad_2,
|
||||
&i.ID_3,
|
||||
&i.Username,
|
||||
&i.Verified,
|
||||
&i.RegistrationDate,
|
||||
&i.Role,
|
||||
&i.Deleted_2,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWishlistsByUsernameWithPrivacy = `-- name: GetWishlistsByUsernameWithPrivacy :many
|
||||
SELECT
|
||||
wl.id, wl.guid, wl.profile_id, wl.hidden, wl.name, wl.icon_name, wl.color, wl.color_grad, wl.deleted,
|
||||
CASE
|
||||
WHEN (ps.hide_profile_details OR ps.hide_for_unauthenticated) THEN FALSE
|
||||
WHEN (
|
||||
ps.hide_profile_details OR (
|
||||
ps.hide_for_unauthenticated AND
|
||||
$1::text = ''
|
||||
)
|
||||
) THEN FALSE
|
||||
ELSE TRUE
|
||||
END AS access_allowed
|
||||
FROM
|
||||
@@ -950,9 +1208,9 @@ JOIN
|
||||
users AS u ON p.user_id = u.id
|
||||
WHERE
|
||||
wl.deleted IS FALSE AND
|
||||
u.username = $1::text AND
|
||||
u.username = $2::text AND
|
||||
(
|
||||
u.username = $2::text OR
|
||||
u.username = $1::text OR
|
||||
(u.verified IS TRUE AND
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
@@ -965,8 +1223,8 @@ WHERE
|
||||
`
|
||||
|
||||
type GetWishlistsByUsernameWithPrivacyParams struct {
|
||||
Username string
|
||||
Requester string
|
||||
Username string
|
||||
}
|
||||
|
||||
type GetWishlistsByUsernameWithPrivacyRow struct {
|
||||
@@ -975,15 +1233,16 @@ type GetWishlistsByUsernameWithPrivacyRow struct {
|
||||
ProfileID int64
|
||||
Hidden bool
|
||||
Name string
|
||||
IconName *string
|
||||
Color *string
|
||||
ColorGrad *string
|
||||
IconName string
|
||||
Color string
|
||||
ColorGrad string
|
||||
Deleted bool
|
||||
AccessAllowed bool
|
||||
}
|
||||
|
||||
// XXX: Obsolete, use the according access check query instead
|
||||
func (q *Queries) GetWishlistsByUsernameWithPrivacy(ctx context.Context, arg GetWishlistsByUsernameWithPrivacyParams) ([]GetWishlistsByUsernameWithPrivacyRow, error) {
|
||||
rows, err := q.db.Query(ctx, getWishlistsByUsernameWithPrivacy, arg.Username, arg.Requester)
|
||||
rows, err := q.db.Query(ctx, getWishlistsByUsernameWithPrivacy, arg.Requester, arg.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1013,6 +1272,34 @@ func (q *Queries) GetWishlistsByUsernameWithPrivacy(ctx context.Context, arg Get
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const moveWishToWishListWithGuid = `-- name: MoveWishToWishListWithGuid :one
|
||||
WITH updated AS (
|
||||
UPDATE wishes w
|
||||
SET
|
||||
wish_list_id = wl.id,
|
||||
wish_list_guid = ($1::text)::uuid
|
||||
FROM wish_lists wl
|
||||
WHERE
|
||||
wl.guid = ($1::text)::uuid AND
|
||||
wl.profile_id = ( -- Make sure the wish is not moved to another profile
|
||||
SELECT profile_id
|
||||
FROM wish_lists
|
||||
WHERE wish_lists.id = w.wish_list_id
|
||||
)
|
||||
RETURNING w.id
|
||||
)
|
||||
SELECT
|
||||
COUNT(*) > 0 AS target_found
|
||||
FROM updated
|
||||
`
|
||||
|
||||
func (q *Queries) MoveWishToWishListWithGuid(ctx context.Context, wishListGuid string) (bool, error) {
|
||||
row := q.db.QueryRow(ctx, moveWishToWishListWithGuid, wishListGuid)
|
||||
var target_found bool
|
||||
err := row.Scan(&target_found)
|
||||
return target_found, err
|
||||
}
|
||||
|
||||
const pruneExpiredConfirmationCodes = `-- name: PruneExpiredConfirmationCodes :exec
|
||||
DELETE FROM confirmation_codes
|
||||
WHERE expires_at < CURRENT_TIMESTAMP
|
||||
@@ -1280,7 +1567,9 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
|
||||
|
||||
const updateUserByUsername = `-- name: UpdateUserByUsername :exec
|
||||
UPDATE users
|
||||
SET verified = $2, deleted = $3
|
||||
SET
|
||||
verified = COALESCE($2, verified),
|
||||
deleted = COALESCE($3, deleted)
|
||||
WHERE username = $1
|
||||
`
|
||||
|
||||
|
||||
55
backend/internal/dto/wishList.go
Normal file
55
backend/internal/dto/wishList.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2025 Nikolai Papin
|
||||
//
|
||||
// This file is part of Easywish
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
// the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package dto
|
||||
|
||||
type WishListDto struct {
|
||||
Guid string `json:"guid" mapstructure:"guid"`
|
||||
Name string `json:"name" mapstructure:"name"`
|
||||
Hidden bool `json:"hidden" mapstructure:"hidden"`
|
||||
IconName string `json:"icon_name" mapstructure:"icon_name"`
|
||||
Color string `json:"color" mapstructure:"color"`
|
||||
ColorGrad string `json:"color_grad" mapstructure:"color_grad"`
|
||||
}
|
||||
|
||||
type NewWishListDto struct {
|
||||
Name string `json:"name" mapstructure:"name" binding:"required" validate:"max=32"`
|
||||
Hidden bool `json:"hidden" mapstructure:"hidden"`
|
||||
IconName string `json:"icon_name" mapstructure:"icon_name" validate:"omitempty,max=64"`
|
||||
Color string `json:"color" mapstructure:"color" validate:"omitempty,color_hex"`
|
||||
ColorGrad string `json:"color_grad" mapstructure:"color_grad" validate:"omitempty,color_hex"`
|
||||
}
|
||||
|
||||
type WishDto struct {
|
||||
Guid string `json:"guid" mapstructure:"guid"`
|
||||
WishListGuid string `json:"wish_list_guid" mapstructure:"wish_list_guid"`
|
||||
Name string `json:"name" mapstructure:"name"`
|
||||
Description string `json:"description" mapstructure:"description"`
|
||||
PictureUrl string `json:"picture_url" mapstructure:"picture_url"`
|
||||
Stars int `json:"stars" mapstructure:"stars"`
|
||||
CreationDate int64 `json:"creation_date" mapstructure:"creation_date"`
|
||||
Fulfilled bool `json:"fulfilled" mapstructure:"fulfilled"`
|
||||
FulfilledDate int64 `json:"fulfilled_date" mapstructure:"fulfilled_date"`
|
||||
}
|
||||
|
||||
type NewWishDto struct {
|
||||
WishListGuid string `json:"wish_list_guid" mapstructure:"wish_list_guid" binding:"required" validate:"guid"`
|
||||
Name string `json:"name" mapstructure:"name" binding:"required" validate:"max=64"`
|
||||
Description string `json:"description" mapstructure:"description" validate:"omitempty,max=1000"`
|
||||
PictureUploadId string `json:"picture_upload_id" mapstructure:"picture_upload_id" validate:"omitempty,upload_id=image"`
|
||||
Stars int `json:"stars" mapstructure:"stars" validate:"min=1,max=5"`
|
||||
}
|
||||
36
backend/internal/services/wishlist.go
Normal file
36
backend/internal/services/wishlist.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2025 Nikolai Papin
|
||||
//
|
||||
// This file is part of Easywish
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
// the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"easywish/internal/dto"
|
||||
"easywish/internal/utils/enums"
|
||||
)
|
||||
|
||||
type WishListService interface {
|
||||
CreateWishList(cinfo dto.ClientInfo, object dto.NewWishListDto) (*dto.WishListDto, error)
|
||||
UpdateWishListByGuid(cinfo dto.ClientInfo, guid string, object dto.NewWishDto) (bool, error)
|
||||
DeleteWishListByGuid(cinfo dto.ClientInfo, guid string) (bool, error)
|
||||
GetWishListByGuid(cinfo dto.ClientInfo, guid string) (*dto.WishListDto, error)
|
||||
GetUserWishListsPaginated(cinfo dto.ClientInfo, amount int, page int, sorting enums.Sorting, sortOrder enums.SortOrder) (*[]dto.WishListDto, error)
|
||||
|
||||
CreateWish(cinfo dto.ClientInfo, object dto.NewWishDto) (*dto.WishDto, error)
|
||||
UpdateWish(cinfo dto.ClientInfo, guid string, object dto.NewWishDto) (bool, error)
|
||||
GetWishByGuid(cinfo dto.ClientInfo, guid string) (*dto.WishDto, error)
|
||||
GetWishesByWishListGuidPaginated(cinfo dto.ClientInfo, guid string, amount int, page int, sorting enums.Sorting, sortOrder enums.SortOrder) (*[]dto.WishDto, error)
|
||||
}
|
||||
@@ -35,3 +35,15 @@ const (
|
||||
JwtAccessTokenType JwtTokenType = iota
|
||||
JwtRefreshTokenType
|
||||
)
|
||||
|
||||
type Sorting int32
|
||||
const (
|
||||
ByDate Sorting = iota
|
||||
ByScore
|
||||
)
|
||||
|
||||
type SortOrder int32
|
||||
const (
|
||||
Descending = iota
|
||||
Ascending
|
||||
)
|
||||
|
||||
37
backend/internal/utils/mapSpecial/wishDto.go
Normal file
37
backend/internal/utils/mapSpecial/wishDto.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2025 Nikolai Papin
|
||||
//
|
||||
// This file is part of Easywish
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
// the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package mapspecial
|
||||
|
||||
import (
|
||||
"easywish/internal/database"
|
||||
"easywish/internal/dto"
|
||||
|
||||
"github.com/rafiulgits/go-automapper"
|
||||
)
|
||||
|
||||
func MapWishDto(dbModel database.Wish, dtoModel *dto.WishDto) {
|
||||
if dtoModel == nil {
|
||||
dtoModel = &dto.WishDto{}
|
||||
}
|
||||
automapper.Map(&dbModel, dtoModel, func(src *database.Wish, dst *dto.WishDto) {
|
||||
dst.Guid = src.Guid.String()
|
||||
dst.WishListGuid = src.WishListGuid.String()
|
||||
dst.CreationDate = src.CreationDate.Time.UnixMilli()
|
||||
dst.FulfilledDate = src.FulfilledDate.Time.UnixMilli()
|
||||
})
|
||||
}
|
||||
35
backend/internal/utils/mapSpecial/wishListDto.go
Normal file
35
backend/internal/utils/mapSpecial/wishListDto.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2025 Nikolai Papin
|
||||
//
|
||||
// This file is part of Easywish
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
// the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package mapspecial
|
||||
|
||||
import (
|
||||
"easywish/internal/database"
|
||||
"easywish/internal/dto"
|
||||
|
||||
"github.com/rafiulgits/go-automapper"
|
||||
)
|
||||
|
||||
func MapWishListDto(dbModel database.WishList, dtoModel *dto.WishListDto) {
|
||||
if dtoModel == nil {
|
||||
dtoModel = &dto.WishListDto{}
|
||||
}
|
||||
automapper.Map(&dbModel, dtoModel, func(src *database.WishList, dst *dto.WishListDto) {
|
||||
dst.Guid = src.Guid.String()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,6 +42,13 @@ func GetCustomHandlers() []CustomValidatorHandler {
|
||||
return regexp.MustCompile(`^[a-zA-Z0-9_]{3,20}$`).MatchString(username)
|
||||
}},
|
||||
|
||||
{
|
||||
FieldName: "guid",
|
||||
Function: func(fl validator.FieldLevel) bool {
|
||||
guid := fl.Field().String()
|
||||
return regexp.MustCompile(`^([{(]?([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})[})]?)$`).MatchString(guid)
|
||||
}},
|
||||
|
||||
{
|
||||
FieldName: "name",
|
||||
Function: func(fl validator.FieldLevel) bool {
|
||||
|
||||
123
sqlc/query.sql
123
sqlc/query.sql
@@ -280,7 +280,27 @@ SELECT profiles.* FROM profiles
|
||||
JOIN users ON users.id = profiles.user_id
|
||||
WHERE users.username = $1;
|
||||
|
||||
;-- name: CheckProfileAccess :one
|
||||
-- XXX: recheck, was tired
|
||||
SELECT
|
||||
CASE WHEN u.deleted OR NOT u.verified THEN TRUE ELSE FALSE END AS user_unavailable,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM banned_users
|
||||
WHERE user_id = u.id AND
|
||||
pardoned IS FALSE AND
|
||||
(expires_at IS NULL OR expires_at < CURRENT_TIMESTAMP)
|
||||
) THEN TRUE ELSE FALSE END AS user_banned,
|
||||
CASE WHEN ps.hide_profile_details THEN TRUE ELSE FALSE END AS hidden,
|
||||
CASE WHEN ps.hide_for_unauthenticated AND @requester::text = '' THEN TRUE ELSE FALSE END AS auth_required,
|
||||
CASE WHEN ps.captcha THEN TRUE ELSE FALSE END AS captcha_required
|
||||
FROM profiles p
|
||||
JOIN profile_settings ps ON ps.profile_id = p.id
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.id = $1;
|
||||
|
||||
;-- name: GetProfileByUsernameWithPrivacy :one
|
||||
-- FIXME: tweak backend code to handle privacy correctly
|
||||
SELECT
|
||||
u.username,
|
||||
p.name,
|
||||
@@ -288,27 +308,13 @@ SELECT
|
||||
p.avatar_url,
|
||||
CASE WHEN ps.hide_birthday THEN NULL ELSE p.birthday END AS birthday,
|
||||
p.color,
|
||||
p.color_grad,
|
||||
NOT (@requester::text = '' AND ps.hide_for_unauthenticated) AS access_allowed
|
||||
p.color_grad
|
||||
FROM
|
||||
users AS u
|
||||
JOIN profiles AS p ON u.id = p.user_id
|
||||
JOIN profile_settings AS ps ON p.id = ps.profile_id
|
||||
WHERE
|
||||
u.username = @searched_username::text
|
||||
AND (
|
||||
@searched_username::text = @requester::text
|
||||
OR
|
||||
u.deleted IS FALSE
|
||||
AND u.verified IS TRUE
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM banned_users
|
||||
WHERE user_id = u.id AND
|
||||
pardoned IS FALSE AND
|
||||
(expires_at IS NULL OR expires_at < CURRENT_TIMESTAMP)
|
||||
)
|
||||
);
|
||||
u.username = @searched_username::text;
|
||||
|
||||
;-- name: GetProfilesRestricted :many
|
||||
SELECT
|
||||
@@ -389,11 +395,23 @@ WHERE wl.guid = (@guid::text)::uuid;
|
||||
SELECT * FROM wish_lists wl
|
||||
WHERE wl.guid = (@guid::text)::uuid;
|
||||
|
||||
;-- name: GetWishlistsByUsername :many
|
||||
SELECT * FROM wish_lists wl
|
||||
JOIN profiles p ON p.id = wl.profile_id
|
||||
JOIN users u ON u.id = p.user_id
|
||||
WHERE u.username = @username::text;
|
||||
|
||||
-- name: GetWishlistsByUsernameWithPrivacy :many
|
||||
-- XXX: Obsolete, create according access check query instead
|
||||
SELECT
|
||||
wl.*,
|
||||
CASE
|
||||
WHEN (ps.hide_profile_details OR ps.hide_for_unauthenticated) THEN FALSE
|
||||
WHEN (
|
||||
ps.hide_profile_details OR (
|
||||
ps.hide_for_unauthenticated AND
|
||||
@requester::text = ''
|
||||
)
|
||||
) THEN FALSE
|
||||
ELSE TRUE
|
||||
END AS access_allowed
|
||||
FROM
|
||||
@@ -423,6 +441,25 @@ WHERE
|
||||
|
||||
--: Wish Object {{{
|
||||
|
||||
;-- name: CreateWish :one
|
||||
INSERT INTO wishes(
|
||||
wish_list_id,
|
||||
wish_list_guid,
|
||||
name,
|
||||
description,
|
||||
picture_url,
|
||||
stars)
|
||||
VALUES
|
||||
(
|
||||
(SELECT id FROM wish_lists wl WHERE wl.guid = (@wish_list_guid::text)::uuid),
|
||||
(@wish_list_guid::text)::uuid,
|
||||
@name::text,
|
||||
@description::text,
|
||||
@picture_url::text,
|
||||
@stars::smallint
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
;-- name: UpdateWishByGuid :exec
|
||||
UPDATE wishes w
|
||||
SET
|
||||
@@ -435,4 +472,56 @@ SET
|
||||
deleted = COALESCE(@deleted::boolean, w.deleted)
|
||||
WHERE w.guid = (@guid::text)::uuid;
|
||||
|
||||
;-- name: MoveWishToWishListWithGuid :one
|
||||
WITH updated AS (
|
||||
UPDATE wishes w
|
||||
SET
|
||||
wish_list_id = wl.id,
|
||||
wish_list_guid = (@wish_list_guid::text)::uuid
|
||||
FROM wish_lists wl
|
||||
WHERE
|
||||
wl.guid = (@wish_list_guid::text)::uuid AND
|
||||
wl.profile_id = ( -- Make sure the wish is not moved to another profile
|
||||
SELECT profile_id
|
||||
FROM wish_lists
|
||||
WHERE wish_lists.id = w.wish_list_id
|
||||
)
|
||||
RETURNING w.id
|
||||
)
|
||||
SELECT
|
||||
COUNT(*) > 0 AS target_found
|
||||
FROM updated;
|
||||
|
||||
;-- name: GetWishByGuid :one
|
||||
SELECT * FROM wishes w
|
||||
WHERE w.guid = (@guid::text)::uuid;
|
||||
|
||||
;-- name: GetWishByGuidWithPrivacy :one
|
||||
-- XXX: Obsolete, create according access check query instead
|
||||
SELECT
|
||||
w.*,
|
||||
CASE
|
||||
WHEN
|
||||
(
|
||||
@requester::text = u.username OR
|
||||
NOT ps.hide_profile_details AND
|
||||
NOT
|
||||
(
|
||||
ps.hide_for_unauthenticated AND
|
||||
@requester::text = ''
|
||||
) AND
|
||||
NOT wl.hidden
|
||||
)
|
||||
THEN TRUE
|
||||
ELSE FALSE
|
||||
END AS access_allowed
|
||||
FROM wishes w
|
||||
JOIN wish_lists wl ON w.wish_list_id = wl.id
|
||||
JOIN profiles p ON wl.profile_id = p.id
|
||||
JOIN profile_settings ps ON ps.profile_id = p.id
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE
|
||||
w.guid = (@guid::text)::uuid AND
|
||||
w.deleted IS FALSE;
|
||||
|
||||
--: }}}
|
||||
|
||||
@@ -101,9 +101,9 @@ CREATE TABLE IF NOT EXISTS "wish_lists" (
|
||||
profile_id BIGINT UNIQUE NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
hidden BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
name VARCHAR(32) NOT NULL DEFAULT 'Wishes',
|
||||
icon_name VARCHAR(64),
|
||||
color VARCHAR(7),
|
||||
color_grad VARCHAR(7),
|
||||
icon_name VARCHAR(64) NOT NULL DEFAULT '',
|
||||
color VARCHAR(7) NOT NULL DEFAULT '',
|
||||
color_grad VARCHAR(7) NOT NULL DEFAULT '',
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
@@ -111,7 +111,8 @@ CREATE TABLE IF NOT EXISTS "wishes" (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
guid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
|
||||
wish_list_id BIGINT UNIQUE NOT NULL REFERENCES wish_lists(id) ON DELETE CASCADE,
|
||||
name VARCHAR(32) NOT NULL DEFAULT 'New wish',
|
||||
wish_list_guid UUID NOT NULL REFERENCES wish_lists(guid) ON DELETE CASCADE,
|
||||
name VARCHAR(64) NOT NULL DEFAULT 'New wish',
|
||||
description VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
picture_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
stars SMALLINT NOT NULL DEFAULT 3 CHECK (stars BETWEEN 1 AND 5),
|
||||
@@ -120,3 +121,63 @@ CREATE TABLE IF NOT EXISTS "wishes" (
|
||||
fulfilled_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_profile(requester_user_id BIGINT, target_profile_id BIGINT)
|
||||
RETURNS JSONB AS $$
|
||||
DECLARE
|
||||
profile_record profiles%ROWTYPE;
|
||||
settings_record profile_settings%ROWTYPE;
|
||||
is_owner BOOLEAN;
|
||||
is_banned BOOLEAN;
|
||||
is_deleted BOOLEAN;
|
||||
BEGIN
|
||||
-- Check if target user exists and is not deleted/banned
|
||||
SELECT p.*, u.deleted INTO profile_record
|
||||
FROM profiles p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.id = target_profile_id;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RETURN NULL; -- Or raise an exception for "not found"
|
||||
END IF;
|
||||
|
||||
is_deleted := profile_record.deleted; -- From users table
|
||||
IF is_deleted THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
-- Check if requester is banned (simplified; expand as needed)
|
||||
SELECT EXISTS(SELECT 1 FROM banned_users WHERE user_id = requester_user_id AND pardoned = FALSE AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)) INTO is_banned;
|
||||
IF is_banned THEN
|
||||
RAISE EXCEPTION 'Requester is banned';
|
||||
END IF;
|
||||
|
||||
-- Determine ownership
|
||||
is_owner := (profile_record.user_id = requester_user_id);
|
||||
|
||||
-- Fetch settings
|
||||
SELECT * INTO settings_record FROM profile_settings WHERE profile_id = target_profile_id;
|
||||
|
||||
-- Apply privacy: Hide for unauthenticated or based on settings
|
||||
IF requester_user_id IS NULL AND settings_record.hide_for_unauthenticated THEN -- NULL requester means unauth
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
IF NOT is_owner AND settings_record.hide_profile_details THEN
|
||||
RETURN NULL; -- Or return minimal public data
|
||||
END IF;
|
||||
|
||||
-- Sanitize fields based on settings
|
||||
IF NOT is_owner AND settings_record.hide_birthday THEN
|
||||
profile_record.birthday := NULL;
|
||||
END IF;
|
||||
-- Add more field-level masking here (e.g., bio, avatar_url)
|
||||
|
||||
-- Return as JSONB for easy app consumption
|
||||
RETURN row_to_json(profile_record)::JSONB;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RAISE NOTICE 'Access denied: %', SQLERRM;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
Reference in New Issue
Block a user