diff --git a/backend/internal/database/models.go b/backend/internal/database/models.go index 5ca3d0a..3acf1d7 100644 --- a/backend/internal/database/models.go +++ b/backend/internal/database/models.go @@ -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 +} diff --git a/backend/internal/database/query.sql.go b/backend/internal/database/query.sql.go index 5d8c06a..272a07f 100644 --- a/backend/internal/database/query.sql.go +++ b/backend/internal/database/query.sql.go @@ -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 @@ -369,7 +417,9 @@ WHERE AND NOT EXISTS ( SELECT 1 FROM banned_users - WHERE user_id = u.id + WHERE user_id = u.id AND + pardoned IS FALSE AND + (expires_at IS NULL OR expires_at < CURRENT_TIMESTAMP) ) ) ` @@ -726,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 @@ -777,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) ` @@ -794,7 +849,7 @@ type GetValidUserByLoginCredentialsParams struct { type GetValidUserByLoginCredentialsRow struct { ID int64 Username string - Verified *bool + Verified bool RegistrationDate pgtype.Timestamp Role int32 Deleted *bool @@ -856,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 @@ -920,7 +1077,7 @@ type UpdateBannedUserParams struct { Reason *string ExpiresAt pgtype.Timestamp BannedBy *string - Pardoned *bool + Pardoned bool PardonedBy *string } @@ -946,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 { @@ -1112,7 +1269,7 @@ WHERE id = $1 type UpdateUserParams struct { ID int64 - Verified *bool + Verified bool Deleted *bool } @@ -1129,7 +1286,7 @@ WHERE username = $1 type UpdateUserByUsernameParams struct { Username string - Verified *bool + Verified bool Deleted *bool } @@ -1137,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 +} diff --git a/backend/internal/services/auth.go b/backend/internal/services/auth.go index 489faaa..6f7eafd 100644 --- a/backend/internal/services/auth.go +++ b/backend/internal/services/auth.go @@ -414,7 +414,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 { @@ -428,7 +428,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 { @@ -864,7 +864,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", diff --git a/sqlc/query.sql b/sqlc/query.sql index 55c6b75..a312f5a 100644 --- a/sqlc/query.sql +++ b/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 @@ -320,7 +304,9 @@ WHERE AND NOT EXISTS ( SELECT 1 FROM banned_users - WHERE user_id = u.id + WHERE user_id = u.id AND + pardoned IS FALSE AND + (expires_at IS NULL OR expires_at < CURRENT_TIMESTAMP) ) ); @@ -371,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; + +--: }}} diff --git a/sqlc/schema.sql b/sqlc/schema.sql index 9faa747..831e3b5 100644 --- a/sqlc/schema.sql +++ b/sqlc/schema.sql @@ -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 +);