Compare commits
7 Commits
5ed75c350a
...
fix-auth_s
| Author | SHA1 | Date | |
|---|---|---|---|
| a5f89c2b32 | |||
| ffcbff5294 | |||
| d7d18f1284 | |||
| b1125d3f6a | |||
| 3bcd8af100 | |||
| b24ffcf3f8 | |||
| 3a63a14c4d |
@@ -15,7 +15,7 @@ type BannedUser struct {
|
||||
Reason *string
|
||||
ExpiresAt pgtype.Timestamp
|
||||
BannedBy *string
|
||||
Pardoned *bool
|
||||
Pardoned bool
|
||||
PardonedBy *string
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ type ConfirmationCode struct {
|
||||
CodeType int32
|
||||
CodeHash string
|
||||
ExpiresAt pgtype.Timestamp
|
||||
Used *bool
|
||||
Deleted *bool
|
||||
Used bool
|
||||
Deleted bool
|
||||
}
|
||||
|
||||
type LoginInformation struct {
|
||||
@@ -78,8 +78,34 @@ type Session struct {
|
||||
type User struct {
|
||||
ID int64
|
||||
Username string
|
||||
Verified *bool
|
||||
Verified bool
|
||||
RegistrationDate pgtype.Timestamp
|
||||
Role int32
|
||||
Deleted *bool
|
||||
}
|
||||
|
||||
type Wish struct {
|
||||
ID int64
|
||||
Guid pgtype.UUID
|
||||
WishListID int64
|
||||
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
|
||||
}
|
||||
|
||||
@@ -253,6 +253,54 @@ func (q *Queries) CreateUser(ctx context.Context, username string) (User, error)
|
||||
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::boolean
|
||||
)
|
||||
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 bool
|
||||
}
|
||||
|
||||
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
|
||||
WITH deleted_rows AS (
|
||||
DELETE FROM users
|
||||
@@ -345,59 +393,65 @@ func (q *Queries) GetProfileByUsername(ctx context.Context, username string) (Pr
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getProfileByUsernameRestricted = `-- name: GetProfileByUsernameRestricted :one
|
||||
const getProfileByUsernameWithPrivacy = `-- name: GetProfileByUsernameWithPrivacy :one
|
||||
SELECT
|
||||
users.username,
|
||||
profiles.name,
|
||||
CASE
|
||||
WHEN profile_settings.hide_birthday OR profile_settings.hide_profile_details THEN NULL
|
||||
ELSE profiles.birthday
|
||||
END AS birthday,
|
||||
CASE
|
||||
WHEN profile_settings.hide_profile_details THEN NULL
|
||||
ELSE profiles.bio
|
||||
END AS bio,
|
||||
CASE
|
||||
WHEN profile_settings.hide_profile_details THEN NULL
|
||||
ELSE profiles.avatar_url
|
||||
END AS avatar_url,
|
||||
profiles.color,
|
||||
profiles.color_grad,
|
||||
profile_settings.hide_profile_details
|
||||
FROM profiles
|
||||
JOIN users ON users.id = profiles.user_id
|
||||
JOIN profile_settings ON profiles.id = profile_settings.profile_id
|
||||
WHERE users.username = $1 AND ($2 IS FALSE OR profile_settings.hide_for_unauthenticated IS FALSE)
|
||||
u.username,
|
||||
p.name,
|
||||
p.bio,
|
||||
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
|
||||
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)
|
||||
)
|
||||
)
|
||||
`
|
||||
|
||||
type GetProfileByUsernameRestrictedParams struct {
|
||||
Username string
|
||||
Column2 *bool
|
||||
type GetProfileByUsernameWithPrivacyParams struct {
|
||||
Requester string
|
||||
SearchedUsername string
|
||||
}
|
||||
|
||||
type GetProfileByUsernameRestrictedRow struct {
|
||||
type GetProfileByUsernameWithPrivacyRow struct {
|
||||
Username string
|
||||
Name string
|
||||
Bio string
|
||||
AvatarUrl string
|
||||
Birthday pgtype.Timestamp
|
||||
Bio *string
|
||||
AvatarUrl *string
|
||||
Color string
|
||||
ColorGrad string
|
||||
HideProfileDetails bool
|
||||
AccessAllowed *bool
|
||||
}
|
||||
|
||||
func (q *Queries) GetProfileByUsernameRestricted(ctx context.Context, arg GetProfileByUsernameRestrictedParams) (GetProfileByUsernameRestrictedRow, error) {
|
||||
row := q.db.QueryRow(ctx, getProfileByUsernameRestricted, arg.Username, arg.Column2)
|
||||
var i GetProfileByUsernameRestrictedRow
|
||||
func (q *Queries) GetProfileByUsernameWithPrivacy(ctx context.Context, arg GetProfileByUsernameWithPrivacyParams) (GetProfileByUsernameWithPrivacyRow, error) {
|
||||
row := q.db.QueryRow(ctx, getProfileByUsernameWithPrivacy, arg.Requester, arg.SearchedUsername)
|
||||
var i GetProfileByUsernameWithPrivacyRow
|
||||
err := row.Scan(
|
||||
&i.Username,
|
||||
&i.Name,
|
||||
&i.Birthday,
|
||||
&i.Bio,
|
||||
&i.AvatarUrl,
|
||||
&i.Birthday,
|
||||
&i.Color,
|
||||
&i.ColorGrad,
|
||||
&i.HideProfileDetails,
|
||||
&i.AccessAllowed,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -722,11 +776,11 @@ type GetValidConfirmationCodesByUsernameRow struct {
|
||||
CodeType int32
|
||||
CodeHash string
|
||||
ExpiresAt pgtype.Timestamp
|
||||
Used *bool
|
||||
Deleted *bool
|
||||
Used bool
|
||||
Deleted bool
|
||||
ID_2 int64
|
||||
Username string
|
||||
Verified *bool
|
||||
Verified bool
|
||||
RegistrationDate pgtype.Timestamp
|
||||
Role int32
|
||||
Deleted_2 *bool
|
||||
@@ -773,12 +827,17 @@ SELECT
|
||||
linfo.totp_encrypted
|
||||
FROM users
|
||||
JOIN login_informations AS linfo ON users.id = linfo.user_id
|
||||
LEFT JOIN banned_users AS banned ON users.id = banned.user_id
|
||||
WHERE
|
||||
users.username = $1 AND
|
||||
users.verified IS TRUE AND -- Verified
|
||||
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)
|
||||
`
|
||||
|
||||
@@ -790,7 +849,7 @@ type GetValidUserByLoginCredentialsParams struct {
|
||||
type GetValidUserByLoginCredentialsRow struct {
|
||||
ID int64
|
||||
Username string
|
||||
Verified *bool
|
||||
Verified bool
|
||||
RegistrationDate pgtype.Timestamp
|
||||
Role int32
|
||||
Deleted *bool
|
||||
@@ -852,6 +911,108 @@ func (q *Queries) GetValidUserSessions(ctx context.Context, userID int64) ([]Ses
|
||||
return items, nil
|
||||
}
|
||||
|
||||
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 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
|
||||
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 = $1::text AND
|
||||
(
|
||||
u.username = $2::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 {
|
||||
Username string
|
||||
Requester 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.Username, arg.Requester)
|
||||
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 pruneExpiredConfirmationCodes = `-- name: PruneExpiredConfirmationCodes :exec
|
||||
DELETE FROM confirmation_codes
|
||||
WHERE expires_at < CURRENT_TIMESTAMP
|
||||
@@ -916,7 +1077,7 @@ type UpdateBannedUserParams struct {
|
||||
Reason *string
|
||||
ExpiresAt pgtype.Timestamp
|
||||
BannedBy *string
|
||||
Pardoned *bool
|
||||
Pardoned bool
|
||||
PardonedBy *string
|
||||
}
|
||||
|
||||
@@ -942,8 +1103,8 @@ WHERE id = $1
|
||||
|
||||
type UpdateConfirmationCodeParams struct {
|
||||
ID int64
|
||||
Used *bool
|
||||
Deleted *bool
|
||||
Used bool
|
||||
Deleted bool
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateConfirmationCode(ctx context.Context, arg UpdateConfirmationCodeParams) error {
|
||||
@@ -1108,7 +1269,7 @@ WHERE id = $1
|
||||
|
||||
type UpdateUserParams struct {
|
||||
ID int64
|
||||
Verified *bool
|
||||
Verified bool
|
||||
Deleted *bool
|
||||
}
|
||||
|
||||
@@ -1125,7 +1286,7 @@ WHERE username = $1
|
||||
|
||||
type UpdateUserByUsernameParams struct {
|
||||
Username string
|
||||
Verified *bool
|
||||
Verified bool
|
||||
Deleted *bool
|
||||
}
|
||||
|
||||
@@ -1133,3 +1294,76 @@ func (q *Queries) UpdateUserByUsername(ctx context.Context, arg UpdateUserByUser
|
||||
_, err := q.db.Exec(ctx, updateUserByUsername, arg.Username, arg.Verified, arg.Deleted)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ type NewProfileDto struct {
|
||||
Name string `json:"name" binding:"required" validate:"name"`
|
||||
Bio string `json:"bio" validate:"bio"`
|
||||
AvatarUploadID string `json:"avatar_upload_id" validate:"omitempty,upload_id=avatar"`
|
||||
Birthday int64 `json:"birthday"`
|
||||
Birthday int64 `json:"birthday" validate:"birthday_unix_milli"`
|
||||
Color string `json:"color" validate:"color_hex"`
|
||||
ColorGrad string `json:"color_grad" validate:"color_hex"`
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ type RegistrationCompleteRequest struct {
|
||||
Username string `json:"username" binding:"required" validate:"username"`
|
||||
VerificationCode string `json:"verification_code" binding:"required" validate:"verification_code=reg"`
|
||||
Name string `json:"name" binding:"required" validate:"name"`
|
||||
Birthday *string `json:"birthday"`
|
||||
Birthday int64 `json:"birthday" validate:"birthday_unix_milli"`
|
||||
}
|
||||
|
||||
type RegistrationCompleteResponse struct {
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -347,6 +348,14 @@ func (a *authServiceImpl) RegistrationBegin(request models.RegistrationBeginRequ
|
||||
a.log.Error(
|
||||
"Failed to commit transaction",
|
||||
zap.Error(err))
|
||||
|
||||
redisErr := a.redis.Del(context.TODO(), fmt.Sprintf("email::%s::registration_in_progress", request.Email))
|
||||
if redisErr != nil {
|
||||
a.log.Error(
|
||||
"Failed to delete cooldown redis key while rolling back RegistrationBegin",
|
||||
zap.Error(redisErr.Err()))
|
||||
}
|
||||
|
||||
return false, errs.ErrServerError
|
||||
}
|
||||
|
||||
@@ -413,7 +422,7 @@ func (a *authServiceImpl) RegistrationComplete(cinfo dto.ClientInfo, request mod
|
||||
|
||||
err = db.TXQueries.UpdateConfirmationCode(db.CTX, database.UpdateConfirmationCodeParams{
|
||||
ID: confirmationCode.ID,
|
||||
Used: utils.NewPointer(true),
|
||||
Used: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -427,7 +436,7 @@ func (a *authServiceImpl) RegistrationComplete(cinfo dto.ClientInfo, request mod
|
||||
|
||||
err = db.TXQueries.UpdateUser(db.CTX, database.UpdateUserParams{
|
||||
ID: user.ID,
|
||||
Verified: utils.NewPointer(true),
|
||||
Verified: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -437,9 +446,15 @@ func (a *authServiceImpl) RegistrationComplete(cinfo dto.ClientInfo, request mod
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
|
||||
birthdayTimestamp := pgtype.Timestamp {
|
||||
Time: time.UnixMilli(request.Birthday),
|
||||
Valid: true,
|
||||
}
|
||||
|
||||
profile, err = db.TXQueries.CreateProfile(db.CTX, database.CreateProfileParams{
|
||||
UserID: user.ID,
|
||||
Name: request.Name,
|
||||
Birthday: birthdayTimestamp,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -801,6 +816,14 @@ func (a *authServiceImpl) PasswordResetBegin(request models.PasswordResetBeginRe
|
||||
a.log.Error(
|
||||
"Failed to commit transaction",
|
||||
zap.Error(err))
|
||||
|
||||
redisErr := a.redis.Del(context.TODO(), fmt.Sprintf("email::%s::reset_cooldown", request.Email))
|
||||
if redisErr != nil {
|
||||
a.log.Error(
|
||||
"Failed to delete cooldown redis key while rolling back PasswordResetBegin",
|
||||
zap.Error(redisErr.Err()))
|
||||
}
|
||||
|
||||
return false, errs.ErrServerError
|
||||
}
|
||||
|
||||
@@ -857,7 +880,7 @@ func (a *authServiceImpl) PasswordResetComplete(request models.PasswordResetComp
|
||||
|
||||
if err = db.TXQueries.UpdateConfirmationCode(db.CTX, database.UpdateConfirmationCodeParams{
|
||||
ID: resetCode.ID,
|
||||
Used: utils.NewPointer(true),
|
||||
Used: true,
|
||||
}); err != nil {
|
||||
a.log.Error(
|
||||
"Failed to invalidate password reset code upon use",
|
||||
@@ -897,6 +920,7 @@ func (a *authServiceImpl) PasswordResetComplete(request models.PasswordResetComp
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: grab client info
|
||||
session, err = db.TXQueries.CreateSession(db.CTX, database.CreateSessionParams{
|
||||
UserID: user.ID,
|
||||
Name: utils.NewPointer("First device"),
|
||||
@@ -940,6 +964,7 @@ func (a *authServiceImpl) PasswordResetComplete(request models.PasswordResetComp
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// XXX: Mechanism for loging out existing sessions currently does not exist
|
||||
func (a *authServiceImpl) ChangePassword(request models.ChangePasswordRequest, uinfo dto.ClientInfo) (bool, error) {
|
||||
|
||||
var err error
|
||||
@@ -974,7 +999,7 @@ func (a *authServiceImpl) ChangePassword(request models.ChangePasswordRequest, u
|
||||
return false, errs.ErrServerError
|
||||
}
|
||||
|
||||
err = db.TXlessQueries.UpdateLoginInformationByUsername(db.CTX, database.UpdateLoginInformationByUsernameParams{
|
||||
err = db.TXQueries.UpdateLoginInformationByUsername(db.CTX, database.UpdateLoginInformationByUsernameParams{
|
||||
Username: uinfo.Username,
|
||||
PasswordHash: newPasswordHash,
|
||||
}); if err != nil {
|
||||
|
||||
@@ -70,7 +70,6 @@ func (p *profileServiceImpl) GetMyProfile(cinfo dto.ClientInfo) (*dto.ProfileDto
|
||||
return profileDto, nil
|
||||
}
|
||||
|
||||
// TODO: Profile privacy settings checks
|
||||
func (p *profileServiceImpl) GetProfileByUsername(cinfo dto.ClientInfo, username string) (*dto.ProfileDto, error) {
|
||||
helper, db, err := database.NewDbHelperTransaction(p.dbctx); if err != nil {
|
||||
p.log.Error(
|
||||
@@ -80,7 +79,10 @@ func (p *profileServiceImpl) GetProfileByUsername(cinfo dto.ClientInfo, username
|
||||
}
|
||||
defer helper.Rollback()
|
||||
|
||||
profile, err := db.TXQueries.GetProfileByUsername(db.CTX, username); if err != nil {
|
||||
profileRow, err := db.TXQueries.GetProfileByUsernameWithPrivacy(db.CTX, database.GetProfileByUsernameWithPrivacyParams{
|
||||
Requester: cinfo.Username,
|
||||
SearchedUsername: username,
|
||||
}); if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, errs.ErrNotFound
|
||||
}
|
||||
@@ -92,8 +94,18 @@ func (p *profileServiceImpl) GetProfileByUsername(cinfo dto.ClientInfo, username
|
||||
return nil, errs.ErrServerError
|
||||
}
|
||||
|
||||
profileDto := &dto.ProfileDto{}
|
||||
mapspecial.MapProfileDto(profile, profileDto)
|
||||
if !*profileRow.AccessAllowed {
|
||||
return nil, errs.ErrForbidden
|
||||
}
|
||||
|
||||
profileDto := &dto.ProfileDto{
|
||||
Name: profileRow.Name,
|
||||
Bio: profileRow.Bio,
|
||||
AvatarUrl: &profileRow.AvatarUrl,
|
||||
Birthday: profileRow.Birthday.Time.UnixMilli(),
|
||||
Color: profileRow.Color,
|
||||
ColorGrad: profileRow.ColorGrad,
|
||||
}
|
||||
|
||||
return profileDto, nil
|
||||
}
|
||||
@@ -114,7 +126,6 @@ func (p *profileServiceImpl) GetProfileSettings(cinfo dto.ClientInfo) (*dto.Prof
|
||||
return profileSettingsDto, nil
|
||||
}
|
||||
|
||||
// XXX: no validation for timestamps' allowed ranges
|
||||
func (p *profileServiceImpl) UpdateProfile(cinfo dto.ClientInfo, newProfile dto.NewProfileDto) (bool, error) {
|
||||
helper, db, err := database.NewDbHelperTransaction(p.dbctx); if err != nil {
|
||||
p.log.Error(
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"easywish/config"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
@@ -55,6 +56,22 @@ func GetCustomHandlers() []CustomValidatorHandler {
|
||||
return regexp.MustCompile(`^.{1,512}$`).MatchString(username)
|
||||
}},
|
||||
|
||||
{
|
||||
FieldName: "birthday_unix_milli",
|
||||
Function: func(fl validator.FieldLevel) bool {
|
||||
|
||||
timestamp := fl.Field().Int()
|
||||
date := time.UnixMilli(timestamp)
|
||||
currentDate := time.Now()
|
||||
|
||||
age := currentDate.Year() - date.Year()
|
||||
if currentDate.YearDay() < date.YearDay() {
|
||||
age--
|
||||
}
|
||||
|
||||
return age >= 0 && age <= 122
|
||||
}},
|
||||
|
||||
{
|
||||
FieldName: "color_hex",
|
||||
Function: func(fl validator.FieldLevel) bool {
|
||||
|
||||
165
sqlc/query.sql
165
sqlc/query.sql
@@ -32,7 +32,9 @@ WHERE id = $1;
|
||||
|
||||
;-- name: UpdateUserByUsername :exec
|
||||
UPDATE users
|
||||
SET verified = $2, deleted = $3
|
||||
SET
|
||||
verified = COALESCE($2, verified),
|
||||
deleted = COALESCE($3, deleted)
|
||||
WHERE username = $1;
|
||||
|
||||
;-- name: DeleteUser :exec
|
||||
@@ -56,29 +58,6 @@ SELECT users.* FROM users
|
||||
JOIN login_informations linfo ON linfo.user_id = users.id
|
||||
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
|
||||
SELECT
|
||||
users.*,
|
||||
@@ -86,12 +65,17 @@ SELECT
|
||||
linfo.totp_encrypted
|
||||
FROM users
|
||||
JOIN login_informations AS linfo ON users.id = linfo.user_id
|
||||
LEFT JOIN banned_users AS banned ON users.id = banned.user_id
|
||||
WHERE
|
||||
users.username = $1 AND
|
||||
users.verified IS TRUE AND -- Verified
|
||||
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
|
||||
|
||||
;-- name: CheckUserRegistrationAvailability :one
|
||||
@@ -296,29 +280,35 @@ SELECT profiles.* FROM profiles
|
||||
JOIN users ON users.id = profiles.user_id
|
||||
WHERE users.username = $1;
|
||||
|
||||
;-- name: GetProfileByUsernameRestricted :one
|
||||
;-- name: GetProfileByUsernameWithPrivacy :one
|
||||
SELECT
|
||||
users.username,
|
||||
profiles.name,
|
||||
CASE
|
||||
WHEN profile_settings.hide_birthday OR profile_settings.hide_profile_details THEN NULL
|
||||
ELSE profiles.birthday
|
||||
END AS birthday,
|
||||
CASE
|
||||
WHEN profile_settings.hide_profile_details THEN NULL
|
||||
ELSE profiles.bio
|
||||
END AS bio,
|
||||
CASE
|
||||
WHEN profile_settings.hide_profile_details THEN NULL
|
||||
ELSE profiles.avatar_url
|
||||
END AS avatar_url,
|
||||
profiles.color,
|
||||
profiles.color_grad,
|
||||
profile_settings.hide_profile_details
|
||||
FROM profiles
|
||||
JOIN users ON users.id = profiles.user_id
|
||||
JOIN profile_settings ON profiles.id = profile_settings.profile_id
|
||||
WHERE users.username = $1 AND ($2 IS FALSE OR profile_settings.hide_for_unauthenticated IS FALSE);
|
||||
u.username,
|
||||
p.name,
|
||||
p.bio,
|
||||
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
|
||||
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)
|
||||
)
|
||||
);
|
||||
|
||||
;-- name: GetProfilesRestricted :many
|
||||
SELECT
|
||||
@@ -367,3 +357,82 @@ JOIN users ON users.id = profiles.user_id
|
||||
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::boolean
|
||||
)
|
||||
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: GetWishlistsByUsernameWithPrivacy :many
|
||||
SELECT
|
||||
wl.*,
|
||||
CASE
|
||||
WHEN (ps.hide_profile_details OR ps.hide_for_unauthenticated) 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: 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;
|
||||
|
||||
--: }}}
|
||||
|
||||
@@ -22,7 +22,7 @@ CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
CREATE TABLE IF NOT EXISTS "users" (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(20) UNIQUE NOT NULL,
|
||||
verified BOOLEAN DEFAULT FALSE,
|
||||
verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
registration_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
role INTEGER NOT NULL DEFAULT 1, -- enum user
|
||||
deleted BOOLEAN DEFAULT FALSE
|
||||
@@ -35,7 +35,7 @@ CREATE TABLE IF NOT EXISTS "banned_users" (
|
||||
reason VARCHAR(512),
|
||||
expires_at TIMESTAMP,
|
||||
banned_by VARCHAR(20) DEFAULT 'system',
|
||||
pardoned BOOLEAN DEFAULT FALSE,
|
||||
pardoned BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
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_hash VARCHAR(512) NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + INTERVAL '10 minutes',
|
||||
used BOOLEAN DEFAULT FALSE,
|
||||
deleted BOOLEAN DEFAULT FALSE
|
||||
used BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "sessions" (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
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),
|
||||
platform VARCHAR(175),
|
||||
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,
|
||||
name VARCHAR(75) NOT NULL,
|
||||
bio VARCHAR(512) NOT NULL DEFAULT '',
|
||||
avatar_url VARCHAR(255) NOT NULL DEFAULT '',
|
||||
avatar_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
birthday TIMESTAMP,
|
||||
color VARCHAR(7) NOT NULL DEFAULT '#254333',
|
||||
color_grad VARCHAR(7) NOT NULL DEFAULT '#691E4D'
|
||||
@@ -93,4 +93,30 @@ CREATE TABLE IF NOT EXISTS "profile_settings" (
|
||||
hide_dates BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
captcha 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),
|
||||
color VARCHAR(7),
|
||||
color_grad VARCHAR(7),
|
||||
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,
|
||||
name VARCHAR(32) 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