Compare commits
14 Commits
fix-auth_s
...
feat-wishl
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b40a05e2d | |||
| f81e4eaa47 | |||
| 14bad8e7ef | |||
| 3198612e16 | |||
| dd2960a742 | |||
| c7a440e38f | |||
| d12162fc3b | |||
| 711b1ad5d1 | |||
| af69c4fe07 | |||
| 5eb90b18d5 | |||
| bd90fb339f | |||
| e4c879db36 | |||
| 6cb64d5f03 | |||
| b1125d3f6a |
@@ -748,7 +748,7 @@ const docTemplate = `{
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"birthday": {
|
"birthday": {
|
||||||
"type": "string"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|||||||
@@ -744,7 +744,7 @@
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"birthday": {
|
"birthday": {
|
||||||
"type": "string"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ definitions:
|
|||||||
models.RegistrationCompleteRequest:
|
models.RegistrationCompleteRequest:
|
||||||
properties:
|
properties:
|
||||||
birthday:
|
birthday:
|
||||||
type: string
|
type: integer
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
username:
|
username:
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ require (
|
|||||||
github.com/minio/crc64nvme v1.0.2 // indirect
|
github.com/minio/crc64nvme v1.0.2 // indirect
|
||||||
github.com/minio/md5-simd v1.1.2 // indirect
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
github.com/minio/minio-go/v7 v7.0.95 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // 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/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 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
|
||||||
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
|
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-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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
|||||||
@@ -120,8 +120,12 @@ func (ctrl *ProfileController) getProfileByUsername(c *gin.Context) {
|
|||||||
response, err := ctrl.ps.GetProfileByUsername(cinfo, username); if err != nil {
|
response, err := ctrl.ps.GetProfileByUsername(cinfo, username); if err != nil {
|
||||||
if errors.Is(err, errs.ErrNotFound) {
|
if errors.Is(err, errs.ErrNotFound) {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "Profile not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "Profile not found"})
|
||||||
|
} else if errors.Is(err, errs.ErrUnauthorized) {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Profile avaiable to authrorized users only"})
|
||||||
} else if errors.Is(err, errs.ErrForbidden) {
|
} else if errors.Is(err, errs.ErrForbidden) {
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "Access restricted by profile's privacy settings"})
|
c.JSON(http.StatusForbidden, gin.H{"error": "Access restricted by profile's privacy settings"})
|
||||||
|
} else if errors.Is(err, errs.ErrGone) {
|
||||||
|
c.JSON(http.StatusGone, gin.H{"error": "Profile no longer available"})
|
||||||
} else {
|
} else {
|
||||||
c.Status(http.StatusInternalServerError)
|
c.Status(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type BannedUser struct {
|
|||||||
Reason *string
|
Reason *string
|
||||||
ExpiresAt pgtype.Timestamp
|
ExpiresAt pgtype.Timestamp
|
||||||
BannedBy *string
|
BannedBy *string
|
||||||
Pardoned *bool
|
Pardoned bool
|
||||||
PardonedBy *string
|
PardonedBy *string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,8 +25,8 @@ type ConfirmationCode struct {
|
|||||||
CodeType int32
|
CodeType int32
|
||||||
CodeHash string
|
CodeHash string
|
||||||
ExpiresAt pgtype.Timestamp
|
ExpiresAt pgtype.Timestamp
|
||||||
Used *bool
|
Used bool
|
||||||
Deleted *bool
|
Deleted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoginInformation struct {
|
type LoginInformation struct {
|
||||||
@@ -78,8 +78,35 @@ type Session struct {
|
|||||||
type User struct {
|
type User struct {
|
||||||
ID int64
|
ID int64
|
||||||
Username string
|
Username string
|
||||||
Verified *bool
|
Verified bool
|
||||||
RegistrationDate pgtype.Timestamp
|
RegistrationDate pgtype.Timestamp
|
||||||
Role int32
|
Role int32
|
||||||
Deleted *bool
|
Deleted *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Wish 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
|
||||||
|
}
|
||||||
|
|
||||||
|
type WishList struct {
|
||||||
|
ID int64
|
||||||
|
Guid pgtype.UUID
|
||||||
|
ProfileID int64
|
||||||
|
Hidden bool
|
||||||
|
Name string
|
||||||
|
IconName string
|
||||||
|
Color string
|
||||||
|
ColorGrad string
|
||||||
|
Deleted bool
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,51 @@ import (
|
|||||||
"github.com/jackc/pgx/v5/pgtype"
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
const checkUserRegistrationAvailability = `-- name: CheckUserRegistrationAvailability :one
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(CASE WHEN users.username = $1::text THEN 1 END) > 0 AS username_busy,
|
COUNT(CASE WHEN users.username = $1::text THEN 1 END) > 0 AS username_busy,
|
||||||
@@ -53,6 +98,46 @@ func (q *Queries) CheckUserRegistrationAvailability(ctx context.Context, arg Che
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkWishAccessByGuid = `-- name: CheckWishAccessByGuid :one
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
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
|
||||||
|
LEFT JOIN banned_users bu ON u.id = bu.user_id
|
||||||
|
AND bu.pardoned = FALSE
|
||||||
|
AND (bu.expires_at IS NULL OR bu.expires_at > NOW())
|
||||||
|
WHERE w.guid = ($1::text)::uuid
|
||||||
|
AND ps.hide_profile_details = FALSE
|
||||||
|
AND (
|
||||||
|
$2::text != ''
|
||||||
|
OR ps.hide_for_unauthenticated IS FALSE
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
w.fulfilled = FALSE
|
||||||
|
OR ps.hide_fulfilled IS FALSE
|
||||||
|
)
|
||||||
|
AND w.deleted = FALSE
|
||||||
|
AND wl.deleted = FALSE
|
||||||
|
AND u.deleted = FALSE
|
||||||
|
AND bu.id IS NULL -- Ensures owner is not banned
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type CheckWishAccessByGuidParams struct {
|
||||||
|
Guid string
|
||||||
|
Requester string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CheckWishAccessByGuid(ctx context.Context, arg CheckWishAccessByGuidParams) (bool, error) {
|
||||||
|
row := q.db.QueryRow(ctx, checkWishAccessByGuid, arg.Guid, arg.Requester)
|
||||||
|
var exists bool
|
||||||
|
err := row.Scan(&exists)
|
||||||
|
return exists, err
|
||||||
|
}
|
||||||
|
|
||||||
const createBannedUser = `-- name: CreateBannedUser :one
|
const createBannedUser = `-- name: CreateBannedUser :one
|
||||||
INSERT INTO banned_users(user_id, expires_at, reason, banned_by)
|
INSERT INTO banned_users(user_id, expires_at, reason, banned_by)
|
||||||
VALUES ( $1, $2, $3, $4) RETURNING id, user_id, date, reason, expires_at, banned_by, pardoned, pardoned_by
|
VALUES ( $1, $2, $3, $4) RETURNING id, user_id, date, reason, expires_at, banned_by, pardoned, pardoned_by
|
||||||
@@ -253,6 +338,108 @@ func (q *Queries) CreateUser(ctx context.Context, username string) (User, error)
|
|||||||
return i, err
|
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 (
|
||||||
|
(SELECT p.id FROM profiles AS p
|
||||||
|
JOIN users AS u ON u.id = p.user_id
|
||||||
|
WHERE u.username = $1::text),
|
||||||
|
$2::boolean,
|
||||||
|
$3::text,
|
||||||
|
$4::text,
|
||||||
|
$5::text,
|
||||||
|
$6::text
|
||||||
|
)
|
||||||
|
RETURNING id, guid, profile_id, hidden, name, icon_name, color, color_grad, deleted
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateWishListParams struct {
|
||||||
|
Username string
|
||||||
|
Hidden bool
|
||||||
|
Name string
|
||||||
|
IconName string
|
||||||
|
Color string
|
||||||
|
ColorGrad string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateWishList(ctx context.Context, arg CreateWishListParams) (WishList, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createWishList,
|
||||||
|
arg.Username,
|
||||||
|
arg.Hidden,
|
||||||
|
arg.Name,
|
||||||
|
arg.IconName,
|
||||||
|
arg.Color,
|
||||||
|
arg.ColorGrad,
|
||||||
|
)
|
||||||
|
var i WishList
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Guid,
|
||||||
|
&i.ProfileID,
|
||||||
|
&i.Hidden,
|
||||||
|
&i.Name,
|
||||||
|
&i.IconName,
|
||||||
|
&i.Color,
|
||||||
|
&i.ColorGrad,
|
||||||
|
&i.Deleted,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const deleteUnverifiedAccountsHavingUsernameOrEmail = `-- name: DeleteUnverifiedAccountsHavingUsernameOrEmail :one
|
const deleteUnverifiedAccountsHavingUsernameOrEmail = `-- name: DeleteUnverifiedAccountsHavingUsernameOrEmail :one
|
||||||
WITH deleted_rows AS (
|
WITH deleted_rows AS (
|
||||||
DELETE FROM users
|
DELETE FROM users
|
||||||
@@ -353,45 +540,27 @@ SELECT
|
|||||||
p.avatar_url,
|
p.avatar_url,
|
||||||
CASE WHEN ps.hide_birthday THEN NULL ELSE p.birthday END AS birthday,
|
CASE WHEN ps.hide_birthday THEN NULL ELSE p.birthday END AS birthday,
|
||||||
p.color,
|
p.color,
|
||||||
p.color_grad,
|
p.color_grad
|
||||||
NOT ($1::text = '' AND ps.hide_for_unauthenticated) AS access_allowed
|
|
||||||
FROM
|
FROM
|
||||||
users AS u
|
users AS u
|
||||||
JOIN profiles AS p ON u.id = p.user_id
|
JOIN profiles AS p ON u.id = p.user_id
|
||||||
JOIN profile_settings AS ps ON p.id = ps.profile_id
|
JOIN profile_settings AS ps ON p.id = ps.profile_id
|
||||||
WHERE
|
WHERE
|
||||||
u.username = $2::text
|
u.username = $1::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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetProfileByUsernameWithPrivacyParams struct {
|
|
||||||
Requester string
|
|
||||||
SearchedUsername string
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetProfileByUsernameWithPrivacyRow struct {
|
type GetProfileByUsernameWithPrivacyRow struct {
|
||||||
Username string
|
Username string
|
||||||
Name string
|
Name string
|
||||||
Bio string
|
Bio string
|
||||||
AvatarUrl string
|
AvatarUrl string
|
||||||
Birthday pgtype.Timestamp
|
Birthday pgtype.Timestamp
|
||||||
Color string
|
Color string
|
||||||
ColorGrad string
|
ColorGrad string
|
||||||
AccessAllowed *bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetProfileByUsernameWithPrivacy(ctx context.Context, arg GetProfileByUsernameWithPrivacyParams) (GetProfileByUsernameWithPrivacyRow, error) {
|
func (q *Queries) GetProfileByUsernameWithPrivacy(ctx context.Context, searchedUsername string) (GetProfileByUsernameWithPrivacyRow, error) {
|
||||||
row := q.db.QueryRow(ctx, getProfileByUsernameWithPrivacy, arg.Requester, arg.SearchedUsername)
|
row := q.db.QueryRow(ctx, getProfileByUsernameWithPrivacy, searchedUsername)
|
||||||
var i GetProfileByUsernameWithPrivacyRow
|
var i GetProfileByUsernameWithPrivacyRow
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.Username,
|
&i.Username,
|
||||||
@@ -401,7 +570,6 @@ func (q *Queries) GetProfileByUsernameWithPrivacy(ctx context.Context, arg GetPr
|
|||||||
&i.Birthday,
|
&i.Birthday,
|
||||||
&i.Color,
|
&i.Color,
|
||||||
&i.ColorGrad,
|
&i.ColorGrad,
|
||||||
&i.AccessAllowed,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -726,11 +894,11 @@ type GetValidConfirmationCodesByUsernameRow struct {
|
|||||||
CodeType int32
|
CodeType int32
|
||||||
CodeHash string
|
CodeHash string
|
||||||
ExpiresAt pgtype.Timestamp
|
ExpiresAt pgtype.Timestamp
|
||||||
Used *bool
|
Used bool
|
||||||
Deleted *bool
|
Deleted bool
|
||||||
ID_2 int64
|
ID_2 int64
|
||||||
Username string
|
Username string
|
||||||
Verified *bool
|
Verified bool
|
||||||
RegistrationDate pgtype.Timestamp
|
RegistrationDate pgtype.Timestamp
|
||||||
Role int32
|
Role int32
|
||||||
Deleted_2 *bool
|
Deleted_2 *bool
|
||||||
@@ -777,12 +945,17 @@ SELECT
|
|||||||
linfo.totp_encrypted
|
linfo.totp_encrypted
|
||||||
FROM users
|
FROM users
|
||||||
JOIN login_informations AS linfo ON users.id = linfo.user_id
|
JOIN login_informations AS linfo ON users.id = linfo.user_id
|
||||||
LEFT JOIN banned_users AS banned ON users.id = banned.user_id
|
|
||||||
WHERE
|
WHERE
|
||||||
users.username = $1 AND
|
users.username = $1 AND
|
||||||
users.verified IS TRUE AND -- Verified
|
users.verified IS TRUE AND -- Verified
|
||||||
users.deleted IS FALSE AND -- Not deleted
|
users.deleted IS FALSE AND -- Not deleted
|
||||||
banned.user_id IS NULL AND -- Not banned
|
NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM banned_users
|
||||||
|
WHERE user_id = users.id AND
|
||||||
|
pardoned IS FALSE AND
|
||||||
|
(expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
|
||||||
|
) AND -- Not banned
|
||||||
linfo.password_hash = crypt($2::text, linfo.password_hash)
|
linfo.password_hash = crypt($2::text, linfo.password_hash)
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -794,7 +967,7 @@ type GetValidUserByLoginCredentialsParams struct {
|
|||||||
type GetValidUserByLoginCredentialsRow struct {
|
type GetValidUserByLoginCredentialsRow struct {
|
||||||
ID int64
|
ID int64
|
||||||
Username string
|
Username string
|
||||||
Verified *bool
|
Verified bool
|
||||||
RegistrationDate pgtype.Timestamp
|
RegistrationDate pgtype.Timestamp
|
||||||
Role int32
|
Role int32
|
||||||
Deleted *bool
|
Deleted *bool
|
||||||
@@ -856,6 +1029,265 @@ func (q *Queries) GetValidUserSessions(ctx context.Context, userID int64) ([]Ses
|
|||||||
return items, nil
|
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 getWishListOwnerByGuid = `-- name: GetWishListOwnerByGuid :one
|
||||||
|
SELECT u.id, u.username, u.verified, u.registration_date, u.role, u.deleted
|
||||||
|
FROM wish_lists wl
|
||||||
|
JOIN profiles p ON wl.profile_id = p.id
|
||||||
|
JOIN users u ON p.user_id = u.id
|
||||||
|
WHERE wl.guid = ($1::text)::uuid
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetWishListOwnerByGuid(ctx context.Context, guid string) (User, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getWishListOwnerByGuid, guid)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Username,
|
||||||
|
&i.Verified,
|
||||||
|
&i.RegistrationDate,
|
||||||
|
&i.Role,
|
||||||
|
&i.Deleted,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetWishlistByGuid(ctx context.Context, guid string) (WishList, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getWishlistByGuid, guid)
|
||||||
|
var i WishList
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Guid,
|
||||||
|
&i.ProfileID,
|
||||||
|
&i.Hidden,
|
||||||
|
&i.Name,
|
||||||
|
&i.IconName,
|
||||||
|
&i.Color,
|
||||||
|
&i.ColorGrad,
|
||||||
|
&i.Deleted,
|
||||||
|
)
|
||||||
|
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 AND
|
||||||
|
$1::text = ''
|
||||||
|
)
|
||||||
|
) THEN FALSE
|
||||||
|
ELSE TRUE
|
||||||
|
END AS access_allowed
|
||||||
|
FROM
|
||||||
|
wish_lists wl
|
||||||
|
JOIN
|
||||||
|
profiles AS p ON wl.profile_id = p.id
|
||||||
|
JOIN
|
||||||
|
profile_settings AS ps ON ps.profile_id = p.id
|
||||||
|
JOIN
|
||||||
|
users AS u ON p.user_id = u.id
|
||||||
|
WHERE
|
||||||
|
wl.deleted IS FALSE AND
|
||||||
|
u.username = $2::text AND
|
||||||
|
(
|
||||||
|
u.username = $1::text OR
|
||||||
|
(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)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetWishlistsByUsernameWithPrivacyParams struct {
|
||||||
|
Requester string
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetWishlistsByUsernameWithPrivacyRow struct {
|
||||||
|
ID int64
|
||||||
|
Guid pgtype.UUID
|
||||||
|
ProfileID int64
|
||||||
|
Hidden bool
|
||||||
|
Name string
|
||||||
|
IconName string
|
||||||
|
Color string
|
||||||
|
ColorGrad string
|
||||||
|
Deleted bool
|
||||||
|
AccessAllowed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetWishlistsByUsernameWithPrivacy(ctx context.Context, arg GetWishlistsByUsernameWithPrivacyParams) ([]GetWishlistsByUsernameWithPrivacyRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getWishlistsByUsernameWithPrivacy, arg.Requester, arg.Username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetWishlistsByUsernameWithPrivacyRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetWishlistsByUsernameWithPrivacyRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Guid,
|
||||||
|
&i.ProfileID,
|
||||||
|
&i.Hidden,
|
||||||
|
&i.Name,
|
||||||
|
&i.IconName,
|
||||||
|
&i.Color,
|
||||||
|
&i.ColorGrad,
|
||||||
|
&i.Deleted,
|
||||||
|
&i.AccessAllowed,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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
|
const pruneExpiredConfirmationCodes = `-- name: PruneExpiredConfirmationCodes :exec
|
||||||
DELETE FROM confirmation_codes
|
DELETE FROM confirmation_codes
|
||||||
WHERE expires_at < CURRENT_TIMESTAMP
|
WHERE expires_at < CURRENT_TIMESTAMP
|
||||||
@@ -920,7 +1352,7 @@ type UpdateBannedUserParams struct {
|
|||||||
Reason *string
|
Reason *string
|
||||||
ExpiresAt pgtype.Timestamp
|
ExpiresAt pgtype.Timestamp
|
||||||
BannedBy *string
|
BannedBy *string
|
||||||
Pardoned *bool
|
Pardoned bool
|
||||||
PardonedBy *string
|
PardonedBy *string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -946,8 +1378,8 @@ WHERE id = $1
|
|||||||
|
|
||||||
type UpdateConfirmationCodeParams struct {
|
type UpdateConfirmationCodeParams struct {
|
||||||
ID int64
|
ID int64
|
||||||
Used *bool
|
Used bool
|
||||||
Deleted *bool
|
Deleted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdateConfirmationCode(ctx context.Context, arg UpdateConfirmationCodeParams) error {
|
func (q *Queries) UpdateConfirmationCode(ctx context.Context, arg UpdateConfirmationCodeParams) error {
|
||||||
@@ -1112,7 +1544,7 @@ WHERE id = $1
|
|||||||
|
|
||||||
type UpdateUserParams struct {
|
type UpdateUserParams struct {
|
||||||
ID int64
|
ID int64
|
||||||
Verified *bool
|
Verified bool
|
||||||
Deleted *bool
|
Deleted *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1123,13 +1555,15 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
|
|||||||
|
|
||||||
const updateUserByUsername = `-- name: UpdateUserByUsername :exec
|
const updateUserByUsername = `-- name: UpdateUserByUsername :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET verified = $2, deleted = $3
|
SET
|
||||||
|
verified = COALESCE($2, verified),
|
||||||
|
deleted = COALESCE($3, deleted)
|
||||||
WHERE username = $1
|
WHERE username = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateUserByUsernameParams struct {
|
type UpdateUserByUsernameParams struct {
|
||||||
Username string
|
Username string
|
||||||
Verified *bool
|
Verified bool
|
||||||
Deleted *bool
|
Deleted *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1137,3 +1571,76 @@ func (q *Queries) UpdateUserByUsername(ctx context.Context, arg UpdateUserByUser
|
|||||||
_, err := q.db.Exec(ctx, updateUserByUsername, arg.Username, arg.Verified, arg.Deleted)
|
_, err := q.db.Exec(ctx, updateUserByUsername, arg.Username, arg.Verified, arg.Deleted)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateWishByGuid = `-- name: UpdateWishByGuid :exec
|
||||||
|
UPDATE wishes w
|
||||||
|
SET
|
||||||
|
name = COALESCE($1::text, w.name),
|
||||||
|
description = COALESCE($2::text, w.description),
|
||||||
|
picture_url = COALESCE($3::text, w.picture_url),
|
||||||
|
stars = COALESCE($4::smallint, w.stars),
|
||||||
|
fulfilled = COALESCE($5::boolean, w.fulfilled),
|
||||||
|
fulfilled_date = COALESCE($6::timestamp, w.fulfilled_date),
|
||||||
|
deleted = COALESCE($7::boolean, w.deleted)
|
||||||
|
WHERE w.guid = ($8::text)::uuid
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateWishByGuidParams struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
PictureUrl string
|
||||||
|
Stars int16
|
||||||
|
Fulfilled bool
|
||||||
|
FulfilledDate pgtype.Timestamp
|
||||||
|
Deleted bool
|
||||||
|
Guid string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateWishByGuid(ctx context.Context, arg UpdateWishByGuidParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, updateWishByGuid,
|
||||||
|
arg.Name,
|
||||||
|
arg.Description,
|
||||||
|
arg.PictureUrl,
|
||||||
|
arg.Stars,
|
||||||
|
arg.Fulfilled,
|
||||||
|
arg.FulfilledDate,
|
||||||
|
arg.Deleted,
|
||||||
|
arg.Guid,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateWishListByGuid = `-- name: UpdateWishListByGuid :exec
|
||||||
|
UPDATE wish_lists wl
|
||||||
|
SET
|
||||||
|
hidden = COALESCE($1::boolean, wl.hidden),
|
||||||
|
name = COALESCE($2::text, wl.name),
|
||||||
|
icon_name = COALESCE($3::text, wl.icon_name),
|
||||||
|
color = COALESCE($4::text, wl.color),
|
||||||
|
color_grad = COALESCE($5::text, wl.color_grad),
|
||||||
|
deleted = COALESCE($6::boolean, wl.deleted)
|
||||||
|
WHERE wl.guid = ($7::text)::uuid
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateWishListByGuidParams struct {
|
||||||
|
Hidden bool
|
||||||
|
Name string
|
||||||
|
IconName string
|
||||||
|
Color string
|
||||||
|
ColorGrad string
|
||||||
|
Deleted bool
|
||||||
|
Guid string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateWishListByGuid(ctx context.Context, arg UpdateWishListByGuidParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, updateWishListByGuid,
|
||||||
|
arg.Hidden,
|
||||||
|
arg.Name,
|
||||||
|
arg.IconName,
|
||||||
|
arg.Color,
|
||||||
|
arg.ColorGrad,
|
||||||
|
arg.Deleted,
|
||||||
|
arg.Guid,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
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"`
|
||||||
|
}
|
||||||
@@ -27,4 +27,5 @@ var (
|
|||||||
ErrForbidden = errors.New("Access is denied")
|
ErrForbidden = errors.New("Access is denied")
|
||||||
ErrTooManyRequests = errors.New("Too many requests")
|
ErrTooManyRequests = errors.New("Too many requests")
|
||||||
ErrNotFound = errors.New("Resource not found")
|
ErrNotFound = errors.New("Resource not found")
|
||||||
|
ErrGone = errors.New("Resource no longer available")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ func (a *authServiceImpl) RegistrationComplete(cinfo dto.ClientInfo, request mod
|
|||||||
|
|
||||||
err = db.TXQueries.UpdateConfirmationCode(db.CTX, database.UpdateConfirmationCodeParams{
|
err = db.TXQueries.UpdateConfirmationCode(db.CTX, database.UpdateConfirmationCodeParams{
|
||||||
ID: confirmationCode.ID,
|
ID: confirmationCode.ID,
|
||||||
Used: utils.NewPointer(true),
|
Used: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -436,7 +436,7 @@ func (a *authServiceImpl) RegistrationComplete(cinfo dto.ClientInfo, request mod
|
|||||||
|
|
||||||
err = db.TXQueries.UpdateUser(db.CTX, database.UpdateUserParams{
|
err = db.TXQueries.UpdateUser(db.CTX, database.UpdateUserParams{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
Verified: utils.NewPointer(true),
|
Verified: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -880,7 +880,7 @@ func (a *authServiceImpl) PasswordResetComplete(request models.PasswordResetComp
|
|||||||
|
|
||||||
if err = db.TXQueries.UpdateConfirmationCode(db.CTX, database.UpdateConfirmationCodeParams{
|
if err = db.TXQueries.UpdateConfirmationCode(db.CTX, database.UpdateConfirmationCodeParams{
|
||||||
ID: resetCode.ID,
|
ID: resetCode.ID,
|
||||||
Used: utils.NewPointer(true),
|
Used: true,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
a.log.Error(
|
a.log.Error(
|
||||||
"Failed to invalidate password reset code upon use",
|
"Failed to invalidate password reset code upon use",
|
||||||
|
|||||||
@@ -79,10 +79,7 @@ func (p *profileServiceImpl) GetProfileByUsername(cinfo dto.ClientInfo, username
|
|||||||
}
|
}
|
||||||
defer helper.Rollback()
|
defer helper.Rollback()
|
||||||
|
|
||||||
profileRow, err := db.TXQueries.GetProfileByUsernameWithPrivacy(db.CTX, database.GetProfileByUsernameWithPrivacyParams{
|
profileRow, err := db.TXQueries.GetProfileByUsername(db.CTX, username); if err != nil {
|
||||||
Requester: cinfo.Username,
|
|
||||||
SearchedUsername: username,
|
|
||||||
}); if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
return nil, errs.ErrNotFound
|
return nil, errs.ErrNotFound
|
||||||
}
|
}
|
||||||
@@ -94,9 +91,33 @@ func (p *profileServiceImpl) GetProfileByUsername(cinfo dto.ClientInfo, username
|
|||||||
return nil, errs.ErrServerError
|
return nil, errs.ErrServerError
|
||||||
}
|
}
|
||||||
|
|
||||||
if !*profileRow.AccessAllowed {
|
accessChecks, err := db.TXlessQueries.CheckProfileAccess(db.CTX, database.CheckProfileAccessParams{
|
||||||
|
Requester: cinfo.Username,
|
||||||
|
ID: profileRow.ID,
|
||||||
|
}); if err != nil {
|
||||||
|
p.log.Error(
|
||||||
|
"Failed to check access for given profile",
|
||||||
|
zap.String("profile_owner_username", username),
|
||||||
|
zap.String("requester", cinfo.Username),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
if accessChecks.AuthRequired {
|
||||||
|
return nil, errs.ErrUnauthorized
|
||||||
|
}
|
||||||
|
if accessChecks.Hidden {
|
||||||
return nil, errs.ErrForbidden
|
return nil, errs.ErrForbidden
|
||||||
}
|
}
|
||||||
|
if accessChecks.UserBanned {
|
||||||
|
return nil, errs.ErrGone
|
||||||
|
}
|
||||||
|
if accessChecks.UserUnavailable {
|
||||||
|
return nil, errs.ErrGone
|
||||||
|
}
|
||||||
|
if accessChecks.CaptchaRequired {
|
||||||
|
p.log.Warn("Captcha check is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
profileDto := &dto.ProfileDto{
|
profileDto := &dto.ProfileDto{
|
||||||
Name: profileRow.Name,
|
Name: profileRow.Name,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ type s3ServiceImpl struct {
|
|||||||
imagePolicy minio.PostPolicy
|
imagePolicy minio.PostPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUploadService(_minio *minio.Client, _log *zap.Logger) S3Service {
|
func NewS3Service(_minio *minio.Client, _log *zap.Logger) S3Service {
|
||||||
service := s3ServiceImpl{
|
service := s3ServiceImpl{
|
||||||
minio: _minio,
|
minio: _minio,
|
||||||
log: _log,
|
log: _log,
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ import (
|
|||||||
|
|
||||||
var Module = fx.Module("services",
|
var Module = fx.Module("services",
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
NewUploadService,
|
NewS3Service,
|
||||||
NewSmtpService,
|
NewSmtpService,
|
||||||
NewAuthService,
|
NewAuthService,
|
||||||
NewProfileService,
|
NewProfileService,
|
||||||
|
NewWishListService,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
228
backend/internal/services/wishlist.go
Normal file
228
backend/internal/services/wishlist.go
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
// 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/database"
|
||||||
|
"easywish/internal/dto"
|
||||||
|
errs "easywish/internal/errors"
|
||||||
|
"easywish/internal/utils"
|
||||||
|
"easywish/internal/utils/enums"
|
||||||
|
mapspecial "easywish/internal/utils/mapSpecial"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wishListServiceImpl struct {
|
||||||
|
log *zap.Logger
|
||||||
|
dbctx database.DbContext
|
||||||
|
redis *redis.Client
|
||||||
|
s3 S3Service
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
MoveWishToWishList(cinfo dto.ClientInfo, wishGuid string, wishListGuid string) (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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWishListService(_log *zap.Logger, _dbctx database.DbContext, _redis *redis.Client, _s3 S3Service) WishListService {
|
||||||
|
return wishListServiceImpl{
|
||||||
|
log: _log,
|
||||||
|
dbctx: _dbctx,
|
||||||
|
redis: _redis,
|
||||||
|
s3: _s3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) CreateWish(cinfo dto.ClientInfo, object dto.NewWishDto) (*dto.WishDto, error) {
|
||||||
|
helper, db, err := database.NewDbHelperTransaction(w.dbctx); if err != nil {
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to open transaction",
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
defer helper.Rollback()
|
||||||
|
|
||||||
|
// Check if wish list exists
|
||||||
|
wishList, err := db.TXQueries.GetWishlistByGuid(db.CTX, object.WishListGuid); if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
w.log.Warn(
|
||||||
|
"Attempted to create a wish for a wish list that does not exist",
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.String("wish_list_guid", object.WishListGuid),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to get wishlist for the new wish",
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.String("wish_list_guid", object.WishListGuid),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
wishListOwnerUser, err := db.TXQueries.GetWishListOwnerByGuid(db.CTX, wishList.Guid.String()); if err != nil {
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to get wish list owner",
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.String("wish_list_guid", wishList.Guid.String()),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
if wishListOwnerUser.Username != cinfo.Username {
|
||||||
|
w.log.Warn(
|
||||||
|
"Attempt to create wish in a wish list the user does not own",
|
||||||
|
zap.String("owner_username", wishListOwnerUser.Username),
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.String("wish_list_guid", object.WishListGuid))
|
||||||
|
|
||||||
|
// As usual, we will pretend that it does not exist
|
||||||
|
return nil, errs.ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var avatarUrl *string
|
||||||
|
if object.PictureUploadID != "" {
|
||||||
|
key, err := w.s3.SaveUpload(object.PictureUploadID, "images"); if err != nil {
|
||||||
|
if errors.Is(err, errs.ErrFileNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to save image",
|
||||||
|
zap.String("upload_id", object.PictureUploadID),
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
urlObj := w.s3.GetLocalizedFileUrl(*key, "images")
|
||||||
|
avatarUrl = utils.NewPointer(urlObj.String())
|
||||||
|
} else {
|
||||||
|
avatarUrl = utils.NewPointer("")
|
||||||
|
}
|
||||||
|
|
||||||
|
newWish, err := db.TXQueries.CreateWish(db.CTX, database.CreateWishParams{
|
||||||
|
WishListGuid: object.WishListGuid,
|
||||||
|
Name: object.Name,
|
||||||
|
Description: object.Description,
|
||||||
|
PictureUrl: *avatarUrl,
|
||||||
|
Stars: int16(object.Stars),
|
||||||
|
}); if err != nil {
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to create a new wish",
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.String("wish_list_guid", object.WishListGuid),
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
err = helper.Commit(); if err != nil {
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to commit transaction",
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
wishDto := &dto.WishDto{}
|
||||||
|
mapspecial.MapWishDto(newWish, wishDto)
|
||||||
|
return wishDto, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) CreateWishList(cinfo dto.ClientInfo, object dto.NewWishListDto) (*dto.WishListDto, error) {
|
||||||
|
helper, db, err := database.NewDbHelperTransaction(w.dbctx); if err != nil {
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to open transaction",
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
defer helper.Rollback()
|
||||||
|
|
||||||
|
createdWishList, err := db.TXQueries.CreateWishList(db.CTX, database.CreateWishListParams{
|
||||||
|
Username: cinfo.Username,
|
||||||
|
Hidden: object.Hidden,
|
||||||
|
Name: object.Name,
|
||||||
|
IconName: object.IconName,
|
||||||
|
Color: object.Color,
|
||||||
|
ColorGrad: object.ColorGrad,
|
||||||
|
}); if err != nil {
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to create wish list",
|
||||||
|
zap.String("username", cinfo.Username),
|
||||||
|
zap.Error(err))
|
||||||
|
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
err = helper.Commit(); if err != nil {
|
||||||
|
w.log.Error(
|
||||||
|
"Failed to commit transaction",
|
||||||
|
zap.Error(err))
|
||||||
|
return nil, errs.ErrServerError
|
||||||
|
}
|
||||||
|
|
||||||
|
wishListDto := &dto.WishListDto{}
|
||||||
|
mapspecial.MapWishListDto(createdWishList, wishListDto)
|
||||||
|
|
||||||
|
return wishListDto, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) DeleteWishListByGuid(cinfo dto.ClientInfo, guid string) (bool, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) GetUserWishListsPaginated(cinfo dto.ClientInfo, amount int, page int, sorting enums.Sorting, sortOrder enums.SortOrder) (*[]dto.WishListDto, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) GetWishByGuid(cinfo dto.ClientInfo, guid string) (*dto.WishDto, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) GetWishListByGuid(cinfo dto.ClientInfo, guid string) (*dto.WishListDto, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) GetWishesByWishListGuidPaginated(cinfo dto.ClientInfo, guid string, amount int, page int, sorting enums.Sorting, sortOrder enums.SortOrder) (*[]dto.WishDto, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) MoveWishToWishList(cinfo dto.ClientInfo, wishGuid string, wishListGuid string) (bool, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) UpdateWish(cinfo dto.ClientInfo, guid string, object dto.NewWishDto) (bool, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w wishListServiceImpl) UpdateWishListByGuid(cinfo dto.ClientInfo, guid string, object dto.NewWishDto) (bool, error) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
@@ -35,3 +35,15 @@ const (
|
|||||||
JwtAccessTokenType JwtTokenType = iota
|
JwtAccessTokenType JwtTokenType = iota
|
||||||
JwtRefreshTokenType
|
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)
|
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",
|
FieldName: "name",
|
||||||
Function: func(fl validator.FieldLevel) bool {
|
Function: func(fl validator.FieldLevel) bool {
|
||||||
|
|||||||
237
sqlc/query.sql
237
sqlc/query.sql
@@ -32,7 +32,9 @@ WHERE id = $1;
|
|||||||
|
|
||||||
;-- name: UpdateUserByUsername :exec
|
;-- name: UpdateUserByUsername :exec
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET verified = $2, deleted = $3
|
SET
|
||||||
|
verified = COALESCE($2, verified),
|
||||||
|
deleted = COALESCE($3, deleted)
|
||||||
WHERE username = $1;
|
WHERE username = $1;
|
||||||
|
|
||||||
;-- name: DeleteUser :exec
|
;-- name: DeleteUser :exec
|
||||||
@@ -56,29 +58,6 @@ SELECT users.* FROM users
|
|||||||
JOIN login_informations linfo ON linfo.user_id = users.id
|
JOIN login_informations linfo ON linfo.user_id = users.id
|
||||||
WHERE linfo.email = @email::text;
|
WHERE linfo.email = @email::text;
|
||||||
|
|
||||||
;-- name: CheckUserRegistrationAvailability :one
|
|
||||||
-- SELECT
|
|
||||||
-- COUNT(users.username = @username::text) > 0 AS username_busy,
|
|
||||||
-- COUNT(linfo.email = @email::text) > 0 AS email_busy
|
|
||||||
-- FROM users
|
|
||||||
-- JOIN login_informations AS linfo on linfo.user_id = users.id
|
|
||||||
-- WHERE
|
|
||||||
-- (
|
|
||||||
-- users.username = @username::text OR
|
|
||||||
-- linfo.email = @email::text
|
|
||||||
-- )
|
|
||||||
-- AND
|
|
||||||
-- (
|
|
||||||
-- users.verified IS TRUE OR
|
|
||||||
-- COUNT(
|
|
||||||
-- SELECT confirmation_codes as codes
|
|
||||||
-- JOIN users on users.id = codes.user_id
|
|
||||||
-- WHERE codes.code_type = 0 AND
|
|
||||||
-- codes.deleted IS FALSE AND
|
|
||||||
-- codes.expires_at < CURRENT_TIMESTAMP
|
|
||||||
-- ) = 0;
|
|
||||||
-- )
|
|
||||||
|
|
||||||
;-- name: GetValidUserByLoginCredentials :one
|
;-- name: GetValidUserByLoginCredentials :one
|
||||||
SELECT
|
SELECT
|
||||||
users.*,
|
users.*,
|
||||||
@@ -86,12 +65,17 @@ SELECT
|
|||||||
linfo.totp_encrypted
|
linfo.totp_encrypted
|
||||||
FROM users
|
FROM users
|
||||||
JOIN login_informations AS linfo ON users.id = linfo.user_id
|
JOIN login_informations AS linfo ON users.id = linfo.user_id
|
||||||
LEFT JOIN banned_users AS banned ON users.id = banned.user_id
|
|
||||||
WHERE
|
WHERE
|
||||||
users.username = $1 AND
|
users.username = $1 AND
|
||||||
users.verified IS TRUE AND -- Verified
|
users.verified IS TRUE AND -- Verified
|
||||||
users.deleted IS FALSE AND -- Not deleted
|
users.deleted IS FALSE AND -- Not deleted
|
||||||
banned.user_id IS NULL AND -- Not banned
|
NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM banned_users
|
||||||
|
WHERE user_id = users.id AND
|
||||||
|
pardoned IS FALSE AND
|
||||||
|
(expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
|
||||||
|
) AND -- Not banned
|
||||||
linfo.password_hash = crypt(@password::text, linfo.password_hash); -- Password hash matches
|
linfo.password_hash = crypt(@password::text, linfo.password_hash); -- Password hash matches
|
||||||
|
|
||||||
;-- name: CheckUserRegistrationAvailability :one
|
;-- name: CheckUserRegistrationAvailability :one
|
||||||
@@ -296,6 +280,24 @@ SELECT profiles.* FROM profiles
|
|||||||
JOIN users ON users.id = profiles.user_id
|
JOIN users ON users.id = profiles.user_id
|
||||||
WHERE users.username = $1;
|
WHERE users.username = $1;
|
||||||
|
|
||||||
|
;-- 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 @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
|
;-- name: GetProfileByUsernameWithPrivacy :one
|
||||||
SELECT
|
SELECT
|
||||||
u.username,
|
u.username,
|
||||||
@@ -304,25 +306,13 @@ SELECT
|
|||||||
p.avatar_url,
|
p.avatar_url,
|
||||||
CASE WHEN ps.hide_birthday THEN NULL ELSE p.birthday END AS birthday,
|
CASE WHEN ps.hide_birthday THEN NULL ELSE p.birthday END AS birthday,
|
||||||
p.color,
|
p.color,
|
||||||
p.color_grad,
|
p.color_grad
|
||||||
NOT (@requester::text = '' AND ps.hide_for_unauthenticated) AS access_allowed
|
|
||||||
FROM
|
FROM
|
||||||
users AS u
|
users AS u
|
||||||
JOIN profiles AS p ON u.id = p.user_id
|
JOIN profiles AS p ON u.id = p.user_id
|
||||||
JOIN profile_settings AS ps ON p.id = ps.profile_id
|
JOIN profile_settings AS ps ON p.id = ps.profile_id
|
||||||
WHERE
|
WHERE
|
||||||
u.username = @searched_username::text
|
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
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
;-- name: GetProfilesRestricted :many
|
;-- name: GetProfilesRestricted :many
|
||||||
SELECT
|
SELECT
|
||||||
@@ -371,3 +361,170 @@ JOIN users ON users.id = profiles.user_id
|
|||||||
WHERE users.username = $1;
|
WHERE users.username = $1;
|
||||||
|
|
||||||
--: }}}
|
--: }}}
|
||||||
|
|
||||||
|
--: Wish List Object {{{
|
||||||
|
|
||||||
|
;-- name: CreateWishList :one
|
||||||
|
INSERT INTO wish_lists(profile_id, hidden, name, icon_name, color, color_grad)
|
||||||
|
VALUES (
|
||||||
|
(SELECT p.id FROM profiles AS p
|
||||||
|
JOIN users AS u ON u.id = p.user_id
|
||||||
|
WHERE u.username = @username::text),
|
||||||
|
@hidden::boolean,
|
||||||
|
@name::text,
|
||||||
|
@icon_name::text,
|
||||||
|
@color::text,
|
||||||
|
@color_grad::text
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
;-- name: UpdateWishListByGuid :exec
|
||||||
|
UPDATE wish_lists wl
|
||||||
|
SET
|
||||||
|
hidden = COALESCE(@hidden::boolean, wl.hidden),
|
||||||
|
name = COALESCE(@name::text, wl.name),
|
||||||
|
icon_name = COALESCE(@icon_name::text, wl.icon_name),
|
||||||
|
color = COALESCE(@color::text, wl.color),
|
||||||
|
color_grad = COALESCE(@color_grad::text, wl.color_grad),
|
||||||
|
deleted = COALESCE(@deleted::boolean, wl.deleted)
|
||||||
|
WHERE wl.guid = (@guid::text)::uuid;
|
||||||
|
|
||||||
|
;-- name: GetWishlistByGuid :one
|
||||||
|
SELECT * FROM wish_lists wl
|
||||||
|
WHERE wl.guid = (@guid::text)::uuid;
|
||||||
|
|
||||||
|
;-- name: GetWishListOwnerByGuid :one
|
||||||
|
SELECT u.*
|
||||||
|
FROM wish_lists wl
|
||||||
|
JOIN profiles p ON wl.profile_id = p.id
|
||||||
|
JOIN users u ON p.user_id = u.id
|
||||||
|
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
|
||||||
|
SELECT
|
||||||
|
wl.*,
|
||||||
|
CASE
|
||||||
|
WHEN (
|
||||||
|
ps.hide_profile_details OR (
|
||||||
|
ps.hide_for_unauthenticated AND
|
||||||
|
@requester::text = ''
|
||||||
|
)
|
||||||
|
) THEN FALSE
|
||||||
|
ELSE TRUE
|
||||||
|
END AS access_allowed
|
||||||
|
FROM
|
||||||
|
wish_lists wl
|
||||||
|
JOIN
|
||||||
|
profiles AS p ON wl.profile_id = p.id
|
||||||
|
JOIN
|
||||||
|
profile_settings AS ps ON ps.profile_id = p.id
|
||||||
|
JOIN
|
||||||
|
users AS u ON p.user_id = u.id
|
||||||
|
WHERE
|
||||||
|
wl.deleted IS FALSE AND
|
||||||
|
u.username = @username::text AND
|
||||||
|
(
|
||||||
|
u.username = @requester::text OR
|
||||||
|
(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)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
--: }}}
|
||||||
|
|
||||||
|
--: 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
|
||||||
|
name = COALESCE(@name::text, w.name),
|
||||||
|
description = COALESCE(@description::text, w.description),
|
||||||
|
picture_url = COALESCE(@picture_url::text, w.picture_url),
|
||||||
|
stars = COALESCE(@stars::smallint, w.stars),
|
||||||
|
fulfilled = COALESCE(@fulfilled::boolean, w.fulfilled),
|
||||||
|
fulfilled_date = COALESCE(@fulfilled_date::timestamp, w.fulfilled_date),
|
||||||
|
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: CheckWishAccessByGuid :one
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
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
|
||||||
|
LEFT JOIN banned_users bu ON u.id = bu.user_id
|
||||||
|
AND bu.pardoned = FALSE
|
||||||
|
AND (bu.expires_at IS NULL OR bu.expires_at > NOW())
|
||||||
|
WHERE w.guid = (@guid::text)::uuid
|
||||||
|
AND ps.hide_profile_details = FALSE
|
||||||
|
AND (
|
||||||
|
@requester::text != ''
|
||||||
|
OR ps.hide_for_unauthenticated IS FALSE
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
w.fulfilled = FALSE
|
||||||
|
OR ps.hide_fulfilled IS FALSE
|
||||||
|
)
|
||||||
|
AND w.deleted = FALSE
|
||||||
|
AND wl.deleted = FALSE
|
||||||
|
AND u.deleted = FALSE
|
||||||
|
AND bu.id IS NULL -- Ensures owner is not banned
|
||||||
|
);
|
||||||
|
|
||||||
|
--: }}}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|||||||
CREATE TABLE IF NOT EXISTS "users" (
|
CREATE TABLE IF NOT EXISTS "users" (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
username VARCHAR(20) UNIQUE NOT NULL,
|
username VARCHAR(20) UNIQUE NOT NULL,
|
||||||
verified BOOLEAN DEFAULT FALSE,
|
verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
registration_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
registration_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
role INTEGER NOT NULL DEFAULT 1, -- enum user
|
role INTEGER NOT NULL DEFAULT 1, -- enum user
|
||||||
deleted BOOLEAN DEFAULT FALSE
|
deleted BOOLEAN DEFAULT FALSE
|
||||||
@@ -35,7 +35,7 @@ CREATE TABLE IF NOT EXISTS "banned_users" (
|
|||||||
reason VARCHAR(512),
|
reason VARCHAR(512),
|
||||||
expires_at TIMESTAMP,
|
expires_at TIMESTAMP,
|
||||||
banned_by VARCHAR(20) DEFAULT 'system',
|
banned_by VARCHAR(20) DEFAULT 'system',
|
||||||
pardoned BOOLEAN DEFAULT FALSE,
|
pardoned BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
pardoned_by VARCHAR(20)
|
pardoned_by VARCHAR(20)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -55,14 +55,14 @@ CREATE TABLE IF NOT EXISTS "confirmation_codes" (
|
|||||||
code_type INTEGER NOT NULL CHECK (code_type IN (0, 1)),
|
code_type INTEGER NOT NULL CHECK (code_type IN (0, 1)),
|
||||||
code_hash VARCHAR(512) NOT NULL,
|
code_hash VARCHAR(512) NOT NULL,
|
||||||
expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + INTERVAL '10 minutes',
|
expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + INTERVAL '10 minutes',
|
||||||
used BOOLEAN DEFAULT FALSE,
|
used BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
deleted BOOLEAN DEFAULT FALSE
|
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "sessions" (
|
CREATE TABLE IF NOT EXISTS "sessions" (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
guid UUID NOT NULL DEFAULT gen_random_uuid(),
|
guid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
|
||||||
name VARCHAR(175),
|
name VARCHAR(175),
|
||||||
platform VARCHAR(175),
|
platform VARCHAR(175),
|
||||||
latest_ip VARCHAR(16),
|
latest_ip VARCHAR(16),
|
||||||
@@ -77,7 +77,7 @@ CREATE TABLE IF NOT EXISTS "profiles" (
|
|||||||
user_id BIGINT UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id BIGINT UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
name VARCHAR(75) NOT NULL,
|
name VARCHAR(75) NOT NULL,
|
||||||
bio VARCHAR(512) NOT NULL DEFAULT '',
|
bio VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
avatar_url VARCHAR(255) NOT NULL DEFAULT '',
|
avatar_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
birthday TIMESTAMP,
|
birthday TIMESTAMP,
|
||||||
color VARCHAR(7) NOT NULL DEFAULT '#254333',
|
color VARCHAR(7) NOT NULL DEFAULT '#254333',
|
||||||
color_grad VARCHAR(7) NOT NULL DEFAULT '#691E4D'
|
color_grad VARCHAR(7) NOT NULL DEFAULT '#691E4D'
|
||||||
@@ -93,4 +93,31 @@ CREATE TABLE IF NOT EXISTS "profile_settings" (
|
|||||||
hide_dates BOOLEAN NOT NULL DEFAULT FALSE,
|
hide_dates BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
captcha BOOLEAN NOT NULL DEFAULT FALSE,
|
captcha BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
followers_only_interaction BOOLEAN NOT NULL DEFAULT FALSE
|
followers_only_interaction BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
)
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "wish_lists" (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
guid UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
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) NOT NULL DEFAULT '',
|
||||||
|
color VARCHAR(7) NOT NULL DEFAULT '',
|
||||||
|
color_grad VARCHAR(7) NOT NULL DEFAULT '',
|
||||||
|
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
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),
|
||||||
|
creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
fulfilled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
fulfilled_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user