diff --git a/backend/internal/auth/jwt.go b/backend/internal/auth/jwt.go index 26e8602..63b72de 100644 --- a/backend/internal/auth/jwt.go +++ b/backend/internal/auth/jwt.go @@ -42,7 +42,7 @@ func (j *JWTManager) GenerateAccessToken(userID, plan string) (string, error) { } func (j *JWTManager) GenerateRefreshToken() (string, time.Time) { - token := uuid.NewString() + token := uuid.Must(uuid.NewV7()).String() expiresAt := time.Now().Add(j.refreshDuration) return token, expiresAt } diff --git a/backend/internal/middleware/request_id.go b/backend/internal/middleware/request_id.go index 82ca259..6cfc0d5 100644 --- a/backend/internal/middleware/request_id.go +++ b/backend/internal/middleware/request_id.go @@ -15,7 +15,7 @@ func RequestID(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := r.Header.Get("X-Request-ID") if id == "" { - id = uuid.NewString() + id = uuid.Must(uuid.NewV7()).String() } ctx := context.WithValue(r.Context(), requestIDKey, id) w.Header().Set("X-Request-ID", id) diff --git a/backend/migrations/001_create_users.sql b/backend/migrations/001_create_users.sql index 16646e1..68329a2 100644 --- a/backend/migrations/001_create_users.sql +++ b/backend/migrations/001_create_users.sql @@ -1,5 +1,35 @@ -- +goose Up -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Generate UUID v7 (time-ordered, millisecond precision). +-- Structure: 48-bit unix_ts_ms | 4-bit version (0111) | 12-bit rand_a | 2-bit variant (10) | 62-bit rand_b +CREATE OR REPLACE FUNCTION uuid_generate_v7() +RETURNS uuid +AS $$ +DECLARE + unix_ts_ms bytea; + uuid_bytes bytea; +BEGIN + -- 48-bit Unix timestamp in milliseconds. + -- int8send produces 8 big-endian bytes; skip the first 2 zero bytes to get 6. + unix_ts_ms = substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3); + + -- Use a random v4 UUID as the source of random bits for rand_a and rand_b. + uuid_bytes = uuid_send(gen_random_uuid()); + + -- Overwrite bytes 0–5 with the timestamp (positions 1–6 in 1-indexed bytea). + uuid_bytes = overlay(uuid_bytes placing unix_ts_ms from 1 for 6); + + -- Set version nibble (bits 48–51) to 0111 (7). + uuid_bytes = set_bit(uuid_bytes, 48, 0); + uuid_bytes = set_bit(uuid_bytes, 49, 1); + uuid_bytes = set_bit(uuid_bytes, 50, 1); + uuid_bytes = set_bit(uuid_bytes, 51, 1); + + -- Variant bits (64–65) stay at 10 as inherited from gen_random_uuid(). + + RETURN encode(uuid_bytes, 'hex')::uuid; +END +$$ LANGUAGE plpgsql VOLATILE; CREATE TYPE user_plan AS ENUM ('free', 'paid'); CREATE TYPE user_gender AS ENUM ('male', 'female'); @@ -7,7 +37,7 @@ CREATE TYPE user_goal AS ENUM ('lose', 'maintain', 'gain'); CREATE TYPE activity_level AS ENUM ('low', 'moderate', 'high'); CREATE TABLE users ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), firebase_uid VARCHAR(128) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL DEFAULT '', @@ -47,3 +77,4 @@ DROP TYPE IF EXISTS activity_level; DROP TYPE IF EXISTS user_goal; DROP TYPE IF EXISTS user_gender; DROP TYPE IF EXISTS user_plan; +DROP FUNCTION IF EXISTS uuid_generate_v7(); diff --git a/backend/migrations/002_create_ingredient_mappings.sql b/backend/migrations/002_create_ingredient_mappings.sql index 5faa8b4..0248ebf 100644 --- a/backend/migrations/002_create_ingredient_mappings.sql +++ b/backend/migrations/002_create_ingredient_mappings.sql @@ -1,6 +1,6 @@ -- +goose Up CREATE TABLE ingredient_mappings ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), canonical_name VARCHAR(255) NOT NULL, canonical_name_ru VARCHAR(255), spoonacular_id INTEGER UNIQUE, diff --git a/backend/migrations/003_create_recipes.sql b/backend/migrations/003_create_recipes.sql index 5352593..a5ef370 100644 --- a/backend/migrations/003_create_recipes.sql +++ b/backend/migrations/003_create_recipes.sql @@ -3,7 +3,7 @@ CREATE TYPE recipe_source AS ENUM ('spoonacular', 'ai', 'user'); CREATE TYPE recipe_difficulty AS ENUM ('easy', 'medium', 'hard'); CREATE TABLE recipes ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), source recipe_source NOT NULL DEFAULT 'spoonacular', spoonacular_id INTEGER UNIQUE, diff --git a/backend/migrations/004_create_saved_recipes.sql b/backend/migrations/004_create_saved_recipes.sql index 024fded..c1f14f5 100644 --- a/backend/migrations/004_create_saved_recipes.sql +++ b/backend/migrations/004_create_saved_recipes.sql @@ -1,6 +1,6 @@ -- +goose Up CREATE TABLE saved_recipes ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, title TEXT NOT NULL, description TEXT, diff --git a/backend/migrations/006_create_products.sql b/backend/migrations/006_create_products.sql index c586508..2fb5015 100644 --- a/backend/migrations/006_create_products.sql +++ b/backend/migrations/006_create_products.sql @@ -1,6 +1,6 @@ -- +goose Up CREATE TABLE products ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, mapping_id UUID REFERENCES ingredient_mappings(id), name TEXT NOT NULL, diff --git a/backend/migrations/007_create_menu_plans.sql b/backend/migrations/007_create_menu_plans.sql index d911bea..5999c7b 100644 --- a/backend/migrations/007_create_menu_plans.sql +++ b/backend/migrations/007_create_menu_plans.sql @@ -1,7 +1,7 @@ -- +goose Up CREATE TABLE menu_plans ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, week_start DATE NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), @@ -9,7 +9,7 @@ CREATE TABLE menu_plans ( ); CREATE TABLE menu_items ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), menu_plan_id UUID NOT NULL REFERENCES menu_plans(id) ON DELETE CASCADE, day_of_week INT NOT NULL CHECK (day_of_week BETWEEN 1 AND 7), meal_type TEXT NOT NULL CHECK (meal_type IN ('breakfast','lunch','dinner')), @@ -21,7 +21,7 @@ CREATE TABLE menu_items ( -- Stores the generated shopping list for a menu plan. -- items is a JSONB array of {name, category, amount, unit, checked, in_stock}. CREATE TABLE shopping_lists ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, menu_plan_id UUID REFERENCES menu_plans(id) ON DELETE CASCADE, items JSONB NOT NULL DEFAULT '[]', diff --git a/backend/migrations/008_create_meal_diary.sql b/backend/migrations/008_create_meal_diary.sql index 9b24056..3ac9258 100644 --- a/backend/migrations/008_create_meal_diary.sql +++ b/backend/migrations/008_create_meal_diary.sql @@ -1,7 +1,7 @@ -- +goose Up CREATE TABLE meal_diary ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + id UUID PRIMARY KEY DEFAULT uuid_generate_v7(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, date DATE NOT NULL, meal_type TEXT NOT NULL,