refactor: began refactoring access control in sql

This commit is contained in:
2025-08-16 22:21:41 +03:00
parent c7a440e38f
commit dd2960a742
3 changed files with 443 additions and 54 deletions

View File

@@ -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,47 +501,28 @@ 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
Bio string
AvatarUrl string
Birthday pgtype.Timestamp
Color string
ColorGrad string
AccessAllowed *bool
Username string
Name string
Bio string
AvatarUrl string
Birthday pgtype.Timestamp
Color string
ColorGrad string
}
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
}
@@ -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 {
@@ -982,8 +1240,9 @@ type GetWishlistsByUsernameWithPrivacyRow struct {
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
}