diff --git a/.envrc.example b/.envrc.example deleted file mode 100644 index 6240a1d..0000000 --- a/.envrc.example +++ /dev/null @@ -1,35 +0,0 @@ -# Server Configuration -export ADDR=":8080" -export EXTERNAL_URL="localhost:8080" -export FRONTEND_URL="http://localhost:5173" -export ENV="development" -export ALLOWED_ORIGINS="https://*,http://*" # TODO: Change to the actual allowed origins on production - -# Database Configuration -export DB_ADDR="postgres://postgres:postgres@localhost:5432/lk_tmpl?sslmode=disable" -export DB_MAX_OPEN_CONNS="30" -export DB_MAX_IDLE_CONNS="30" -export DB_MAX_IDLE_TIME="15m" - -# Redis Configuration -export REDIS_ADDR="localhost:6379" -export REDIS_PASSWORD="" -export REDIS_DB="0" -export REDIS_ENABLED="false" - -# Authentication -export BASIC_AUTH_USERNAME="admin" -export BASIC_AUTH_PASSWORD="admin" -export AUTH_TOKEN_SECRET="your-secret-key-here" -export AUTH_TOKEN_EXP="24h" -export AUTH_TOKEN_ISS="lkapi" -export AUTH_TOKEN_AUD="lkapi" - -# Email Configuration -export FROM_EMAIL="your-email@example.com" -export SENDGRID_API_KEY="your-sendgrid-api-key" - -# Rate Limiter -export RATE_LIMITER_REQS_COUNT="20" -export RATE_LIMITER_TIME_FRAME="1m" -export RATE_LIMITER_ENABLED="true" \ No newline at end of file diff --git a/bin/build-errors.log b/bin/build-errors.log index 70974a9..5b7d352 100644 --- a/bin/build-errors.log +++ b/bin/build-errors.log @@ -1 +1 @@ -exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file +exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file diff --git a/bin/main b/bin/main index 35d2349..cc42830 100755 Binary files a/bin/main and b/bin/main differ diff --git a/cmd/api/api.go b/cmd/api/api.go index ff1a4cf..5019184 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -48,9 +48,10 @@ type config struct { } type mailConfig struct { - exp time.Duration - fromEmail string - sendGrid sendGridConfig + inviteExp time.Duration + passwordRefreshExp time.Duration + fromEmail string + sendGrid sendGridConfig } type authConfig struct { @@ -64,10 +65,11 @@ type basicConfig struct { } type tokenConfig struct { - secret string - exp time.Duration - iss string - aud string + secret string + exp time.Duration + refreshExp time.Duration + iss string + aud string } type redisConfig struct { @@ -89,6 +91,8 @@ type dbConfig struct { maxIdleTime time.Duration } +// This `mount` function is responsible for setting up the routing for your API +// using the Chi router. Here's a breakdown of what it does: func (api *api) mount() http.Handler { r := chi.NewRouter() // *CORS @@ -135,29 +139,37 @@ func (api *api) mount() http.Handler { r.Route("/users", func(r chi.Router) { r.Put("/activate/{token}", api.activateUserHandler) + //r.Put("/reset-password/{token}", api.resetPasswordHandler) - r.Route("/{id}", func(r chi.Router) { + r.Route("/me", func(r chi.Router) { r.Use(api.AuthTokenMiddleware) r.Get("/", api.getUserHandler) r.Patch("/", api.checkProfileOwnership(api.updateUserHandler)) - r.Patch("/password", api.checkProfileOwnership(api.updatePasswordHandler)) - r.Put(("/follow"), api.followUserHandler) - r.Put(("/unfollow"), api.unfollowUserHandler) - }) - r.Group(func(r chi.Router) { - r.Use(api.AuthTokenMiddleware) - r.Get("/feed", api.getUserFeedHandler) }) }) r.Route("/authentication", func(r chi.Router) { - r.Post("/user", api.registerUserHandler) + r.Post("/register", api.registerUserHandler) r.Post("/token", api.createTokenHandler) - + r.Post("/forgot-password", api.forgotPasswordHandler) + r.Post("/refresh", api.refreshTokenHandler) }) }) + r.Route("/admin", func(r chi.Router) { + r.Use(api.AuthTokenMiddleware) + + r.Route("/users", func(r chi.Router) { + r.Get("/", api.protectAdminRoutes("moderator", api.getUsersHandler)) + + r.Route("/{id}", func(r chi.Router) { + r.Get("/", api.protectAdminRoutes("moderator", api.getUserByIdHandler)) + }) + }) + + }) + return r } @@ -204,6 +216,12 @@ func (api *api) run(mux http.Handler) error { return err } + if userStore, ok := api.store.Users.(*store.UserStore); ok { + if err := userStore.Close(); err != nil { + api.logger.Errorw("error closing userstore", err) + } + } + api.logger.Infow("server has been shutdown") return nil diff --git a/cmd/api/auth.go b/cmd/api/auth.go index 13ab266..aefa8bf 100644 --- a/cmd/api/auth.go +++ b/cmd/api/auth.go @@ -27,6 +27,15 @@ type UserWithToken struct { Token string `json:"token"` } +type ForgotPasswordPayload struct { + Email string `json:"email" validate:"required,email,max=255"` +} + +type AuthResponse struct { + Token string `json:"token"` + RefreshToken string `json:"refresh_token"` +} + // RegisterUser godoc // // @Summary Register a new User @@ -75,7 +84,7 @@ func (api *api) registerUserHandler(w http.ResponseWriter, r *http.Request) { hashedToken := hex.EncodeToString(hash[:]) // Store the user - err := api.store.Users.CreateAndInvite(ctx, user, hashedToken, api.config.mail.exp) + err := api.store.Users.CreateAndInvite(ctx, user, hashedToken, api.config.mail.inviteExp) if err != nil { switch { case errors.Is(err, store.ErrDuplicateEmail): @@ -201,3 +210,81 @@ func (api *api) createTokenHandler(w http.ResponseWriter, r *http.Request) { return } } + +// ResetPasswordHandler godoc +// +// @Summary Forgot Password +// @Description Forgot Password +// @Tags authentication +// @Accept json +// @Produce json +// @Param payload body ForgotPasswordPayload true "User Credentials" +// @Success 201 {string} string "Token Created" +// @Failure 400 {object} error +// @Failure 401 {object} error +// @Failure 500 {object} error +// @Router /authentication/forgot-password [post] +func (api *api) forgotPasswordHandler(w http.ResponseWriter, r *http.Request) { + var payload ForgotPasswordPayload + if err := readJSON(w, r, &payload); err != nil { + api.badRequestError(w, r, err) + return + } + + if err := Validate.Struct(payload); err != nil { + api.badRequestError(w, r, err) + return + } + + user, err := api.store.Users.GetByEmail(r.Context(), payload.Email) + if err != nil { + switch err { + case store.ErrNotFound: + api.badRequestError(w, r, err) + default: + api.internalServerError(w, r, err) + } + } + + plainToken := uuid.New().String() + + hash := sha256.Sum256([]byte(plainToken)) + hashedToken := hex.EncodeToString(hash[:]) + + //*Store The Request + err = api.store.Users.PasswordResetRequest(r.Context(), user.ID, hashedToken, api.config.mail.passwordRefreshExp) + if err != nil { + api.internalServerError(w, r, err) + return + } + + updateURL := fmt.Sprintf("%s/reset-password/%s", api.config.frontEndURL, plainToken) + + isProdEnv := api.config.env == "production" + vars := struct { + Username string + ServiceName string + UpdateURL string + Email string + CurrentYear string + }{ + Username: user.Username, + ServiceName: "LK_API_Temp", + UpdateURL: updateURL, + Email: user.Email, + CurrentYear: time.Now().Format("2006"), + } + + status, err := api.mailer.Send(mailer.ForgotPasswordTemplate, user.Username, user.Email, vars, !isProdEnv) + if err != nil { + api.internalServerError(w, r, err) + return + } + + api.logger.Infow("email sent", "status code", status) + + if err := api.jsonResponse(w, http.StatusCreated, plainToken); err != nil { + api.internalServerError(w, r, err) + return + } +} diff --git a/cmd/api/feed.go b/cmd/api/feed.go index 90c2783..9297f02 100644 --- a/cmd/api/feed.go +++ b/cmd/api/feed.go @@ -1,7 +1,6 @@ package main import ( - "log" "net/http" "github.com/FernandoVideira/LK_API_Temp/internal/store" @@ -48,7 +47,6 @@ func (api *api) getUserFeedHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() user := getUserFromContext(r) - log.Println(user) feed, err := api.store.Posts.GetUserFeed(ctx, user.ID, fq) if err != nil { api.internalServerError(w, r, err) diff --git a/cmd/api/main.go b/cmd/api/main.go index d17f180..a63e1a8 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -56,8 +56,9 @@ func main() { }, env: env.GetString("ENV", "development"), mail: mailConfig{ - exp: time.Hour * 24 * 3, - fromEmail: env.GetString("FROM_EMAIL", ""), + inviteExp: time.Hour * 24 * 3, + passwordRefreshExp: time.Hour * 24 * 7, + fromEmail: env.GetString("FROM_EMAIL", ""), sendGrid: sendGridConfig{ apiKey: env.GetString("SENDGRID_API_KEY", ""), }, @@ -111,7 +112,10 @@ func main() { cfg.rateLimiter.TimeFrame, ) - store := store.NewStorage(db) + store, err := store.NewStorage(db) + if err != nil { + logger.Fatal(err) + } cacheStorage := cache.NewRedisStorage(redisDb) mailer := mailer.NewSendGrid(cfg.mail.sendGrid.apiKey, cfg.mail.fromEmail) diff --git a/cmd/api/middleware.go b/cmd/api/middleware.go index 00b0c2f..24a7eae 100644 --- a/cmd/api/middleware.go +++ b/cmd/api/middleware.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "errors" "fmt" + "log" "net/http" "strconv" "strings" @@ -104,17 +105,20 @@ func (api *api) postsContextMiddleware(next http.Handler) http.Handler { api.badRequestError(w, r, err) return } + ctx := r.Context() post, err := api.store.Posts.GetByID(ctx, id) + log.Println("post", post) if err != nil { switch { case errors.Is(err, store.ErrNotFound): api.notFoundError(w, r, err) + return default: api.internalServerError(w, r, err) + return } - return } ctx = context.WithValue(ctx, postCtx, post) @@ -161,6 +165,27 @@ func (api *api) checkProfileOwnership(next http.HandlerFunc) http.HandlerFunc { }) } +func (api *api) protectAdminRoutes(requiredRole string, next http.HandlerFunc) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user := getUserFromContext(r) + allowed, err := api.checkRolePrecedence(r.Context(), user, requiredRole) + if err != nil { + api.internalServerError(w, r, err) + return + } + + if !allowed { + api.forbiddenError(w, r, fmt.Errorf("forbidden")) + return + } + + ctx := r.Context() + + ctx = context.WithValue(ctx, roleContextKey, user.Role) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + func (api *api) checkRolePrecedence(ctx context.Context, user *store.User, roleName string) (bool, error) { role, err := api.store.Roles.GetByName(ctx, roleName) if err != nil { diff --git a/cmd/api/users.go b/cmd/api/users.go index 78dfc40..a2cfea3 100644 --- a/cmd/api/users.go +++ b/cmd/api/users.go @@ -3,7 +3,6 @@ package main import ( "context" "errors" - "log" "net/http" "strconv" @@ -12,8 +11,39 @@ import ( ) type userKey string +type roleKey string const userContextKey userKey = "user" +const roleContextKey roleKey = "role" + +// GetUser godoc +// +// @Summary Fetches all users +// @Description Fetches all users +// @Tags admin +// @Accept json +// @Produce json +// @Success 200 {object} store.User +// @Failure 400 {object} error +// @Failure 404 {object} error +// @Failure 500 {object} error +// @Security ApiKeyAuth +// @Router /admin/users [get] +func (api *api) getUsersHandler(w http.ResponseWriter, r *http.Request) { + role := getRoleFromContext(r) + isAdmin := false + if role.Level == 3 { + isAdmin = true + } + + users, err := api.store.Users.GetAll(r.Context(), isAdmin) + if err != nil { + api.internalServerError(w, r, err) + return + } + + api.jsonResponse(w, http.StatusOK, users) +} // GetUser godoc // @@ -78,7 +108,6 @@ type updateUserPayload struct { // @Router /users/{id} [patch] func (api *api) updateUserHandler(w http.ResponseWriter, r *http.Request) { user := getUserFromContext(r) - log.Println(user) var payload updateUserPayload if err := readJSON(w, r, &payload); err != nil { @@ -103,7 +132,9 @@ func (api *api) updateUser(ctx context.Context, user *store.User) error { return err } - api.cacheStore.Users.Delete(ctx, user.ID) + if api.config.redisCfg.enabled { + api.cacheStore.Users.Delete(ctx, user.ID) + } return nil } @@ -300,3 +331,8 @@ func getUserFromContext(r *http.Request) *store.User { user, _ := r.Context().Value(userContextKey).(*store.User) return user } + +func getRoleFromContext(r *http.Request) *store.Role { + role, _ := r.Context().Value(roleContextKey).(*store.Role) + return role +} diff --git a/cmd/migrate/migrations/000013_password_reset.down.sql b/cmd/migrate/migrations/000013_password_reset.down.sql new file mode 100644 index 0000000..8631036 --- /dev/null +++ b/cmd/migrate/migrations/000013_password_reset.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS password_reset_requests; diff --git a/cmd/migrate/migrations/000013_password_reset.up.sql b/cmd/migrate/migrations/000013_password_reset.up.sql new file mode 100644 index 0000000..f46a3c5 --- /dev/null +++ b/cmd/migrate/migrations/000013_password_reset.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS password_reset_requests ( + token bytea PRIMARY KEY, + user_id bigint NOT NULL, + expiry TIMESTAMP(0) + WITH + TIME ZONE NOT NULL +); diff --git a/cmd/migrate/seed/main.go b/cmd/migrate/seed/main.go index 4268a35..20b0a82 100644 --- a/cmd/migrate/seed/main.go +++ b/cmd/migrate/seed/main.go @@ -16,6 +16,10 @@ func main() { } defer conn.Close() - store := store.NewStorage(conn) + store, err := store.NewStorage(conn) + if err != nil { + log.Fatalf("could not create storage: %v", err) + } + db.Seed(store, conn) } diff --git a/docs/docs.go b/docs/docs.go index 1026ceb..6f36702 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -24,6 +24,52 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/authentication/forgot-password": { + "post": { + "description": "Forgot Password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "authentication" + ], + "summary": "Forgot Password", + "parameters": [ + { + "description": "User Credentials", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.ForgotPasswordPayload" + } + } + ], + "responses": { + "201": { + "description": "Token Created", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, "/authentication/token": { "post": { "description": "Create a new token for the user", @@ -795,6 +841,18 @@ const docTemplate = `{ } } }, + "main.ForgotPasswordPayload": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string", + "maxLength": 255 + } + } + }, "main.RegisterUserPayload": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index 545cda2..32710f4 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -16,6 +16,52 @@ }, "basePath": "/v1", "paths": { + "/authentication/forgot-password": { + "post": { + "description": "Forgot Password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "authentication" + ], + "summary": "Forgot Password", + "parameters": [ + { + "description": "User Credentials", + "name": "payload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/main.ForgotPasswordPayload" + } + } + ], + "responses": { + "201": { + "description": "Token Created", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, "/authentication/token": { "post": { "description": "Create a new token for the user", @@ -787,6 +833,18 @@ } } }, + "main.ForgotPasswordPayload": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string", + "maxLength": 255 + } + } + }, "main.RegisterUserPayload": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ac126c4..e898de6 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -29,6 +29,14 @@ definitions: - email - password type: object + main.ForgotPasswordPayload: + properties: + email: + maxLength: 255 + type: string + required: + - email + type: object main.RegisterUserPayload: properties: email: @@ -230,6 +238,37 @@ info: termsOfService: http://swagger.io/terms/ title: LK API Template paths: + /authentication/forgot-password: + post: + consumes: + - application/json + description: Forgot Password + parameters: + - description: User Credentials + in: body + name: payload + required: true + schema: + $ref: '#/definitions/main.ForgotPasswordPayload' + produces: + - application/json + responses: + "201": + description: Token Created + schema: + type: string + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Forgot Password + tags: + - authentication /authentication/token: post: consumes: diff --git a/go.mod b/go.mod index 1e0fa59..7d970af 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( require ( github.com/KyleBanks/depth v1.2.1 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -31,6 +32,8 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 59d7fb3..d5bbd7b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -42,6 +44,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -64,6 +70,7 @@ github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBU github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go index efed29b..2c86630 100644 --- a/internal/mailer/mailer.go +++ b/internal/mailer/mailer.go @@ -3,9 +3,10 @@ package mailer import "embed" const ( - FromName = "LK API" - maxRetries = 3 - UserWelcomeTemplate = "user_activation.tmpl" + FromName = "LK API" + maxRetries = 3 + UserWelcomeTemplate = "user_activation.tmpl" + ForgotPasswordTemplate = "forgot_password.tmpl" ) //go:embed "templates" diff --git a/internal/mailer/templates/forgot_password.tmpl b/internal/mailer/templates/forgot_password.tmpl new file mode 100644 index 0000000..8f39955 --- /dev/null +++ b/internal/mailer/templates/forgot_password.tmpl @@ -0,0 +1,27 @@ +{{define "subject"}} Reset Your Password - {{.ServiceName}} {{end}} + +{{define "body"}} + + + + + + + +

Hello {{.Username}},

+ +

We received a request to reset your password. If you didn't make this request, you can ignore this email.

+ +

To reset your password, click on the link below:

+ +

{{.UpdateURL}}

+ +

If you want to reset your password manually, copy and paste the link above into your browser.

+ +

If you have any questions or need assistance, please contact our support team.

+ +

Thanks,

+

The {{.ServiceName}} Team

+ + +{{end}} diff --git a/internal/ratelimiter/fixed_window.go b/internal/ratelimiter/fixed_window.go index 78bd743..7104a9f 100644 --- a/internal/ratelimiter/fixed_window.go +++ b/internal/ratelimiter/fixed_window.go @@ -1,7 +1,6 @@ package ratelimiter import ( - "log" "sync" "time" ) @@ -26,8 +25,6 @@ func (rl *FixedWindowLimiter) Allow(ip string) (bool, time.Duration) { count, exists := rl.clients[ip] rl.RUnlock() - log.Println(count, exists) - if !exists || count < rl.limit { rl.Lock() if !exists { diff --git a/internal/store/mocks.go b/internal/store/mocks.go index d1818ac..83ceb56 100644 --- a/internal/store/mocks.go +++ b/internal/store/mocks.go @@ -48,6 +48,11 @@ func (m *MockUserStore) CreateAndInvite(ctx context.Context, user *User, token s return args.Error(0) } +func (m *MockUserStore) PasswordResetRequest(ctx context.Context, userID int64, token string, expiration time.Duration) error { + args := m.Called(ctx, userID, token, expiration) + return args.Error(0) +} + func (m *MockUserStore) Delete(ctx context.Context, id int64) error { args := m.Called(id) return args.Error(0) diff --git a/internal/store/roles.go b/internal/store/roles.go index ef297f9..f23ba4f 100644 --- a/internal/store/roles.go +++ b/internal/store/roles.go @@ -14,21 +14,33 @@ type Role struct { type RoleStore struct { db *sql.DB + + getByNameStmt *sql.Stmt } -func (s *RoleStore) GetByName(ctx context.Context, name string) (*Role, error) { - query := ` +func NewRoleStore(db *sql.DB) (*RoleStore, error) { + getByNameStmt, err := db.PrepareContext(context.Background(), ` SELECT id, name, level, description FROM roles WHERE name = $1 - ` + `) + if err != nil { + return nil, err + } + return &RoleStore{ + db: db, + getByNameStmt: getByNameStmt, + }, nil +} + +func (s *RoleStore) GetByName(ctx context.Context, name string) (*Role, error) { ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() role := &Role{} - err := s.db.QueryRowContext(ctx, query, name).Scan( + err := s.getByNameStmt.QueryRowContext(ctx, name).Scan( &role.ID, &role.Name, &role.Level, diff --git a/internal/store/storage.go b/internal/store/storage.go index 3cf6561..8a3e4b5 100644 --- a/internal/store/storage.go +++ b/internal/store/storage.go @@ -31,6 +31,7 @@ type Storage struct { GetByEmail(context.Context, string) (*User, error) Update(context.Context, *User) error CreateAndInvite(context.Context, *User, string, time.Duration) error + PasswordResetRequest(context.Context, int64, string, time.Duration) error Delete(context.Context, int64) error DeleteUser(context.Context, int64) error } @@ -47,14 +48,20 @@ type Storage struct { } } -func NewStorage(db *sql.DB) Storage { +func NewStorage(db *sql.DB) (Storage, error) { + + userStore, roleStore, err := initStores(db) + if err != nil { + return Storage{}, err + } + return Storage{ Posts: &PostStore{db}, - Users: &UserStore{db}, + Users: userStore, Comments: &CommentStore{db}, Followers: &FollowerStore{db}, - Roles: &RoleStore{db}, - } + Roles: roleStore, + }, nil } func withTx(db *sql.DB, ctx context.Context, f func(*sql.Tx) error) error { @@ -70,3 +77,17 @@ func withTx(db *sql.DB, ctx context.Context, f func(*sql.Tx) error) error { return tx.Commit() } + +func initStores(db *sql.DB) (*UserStore, *RoleStore, error) { + userStore, err := NewUserStore(db) + if err != nil { + return &UserStore{}, &RoleStore{}, err + } + + roleStore, err := NewRoleStore(db) + if err != nil { + return &UserStore{}, &RoleStore{}, err + } + + return userStore, roleStore, nil +} diff --git a/internal/store/users.go b/internal/store/users.go index 6679d35..3dcfdad 100644 --- a/internal/store/users.go +++ b/internal/store/users.go @@ -8,6 +8,8 @@ import ( "time" "golang.org/x/crypto/bcrypt" + + sq "github.com/Masterminds/squirrel" ) type User struct { @@ -29,6 +31,8 @@ type password struct { hash []byte } +var psql = sq.StatementBuilder.PlaceholderFormat(sq.Dollar) + func (p *password) Set(password string) error { hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { @@ -42,22 +46,141 @@ func (p *password) Set(password string) error { type UserStore struct { db *sql.DB + + getByIDStmt *sql.Stmt + getByEmailStmt *sql.Stmt + getByInvitationStmt *sql.Stmt + updateStmt *sql.Stmt + deleteStmt *sql.Stmt + inviteCreateStmt *sql.Stmt + invitationDeleteStmt *sql.Stmt + passwordResetRequestCreateStmt *sql.Stmt +} + +func NewUserStore(db *sql.DB) (*UserStore, error) { + store := &UserStore{ + db: db, + } + + var err error + + store.getByIDStmt, err = db.Prepare(` + SELECT u.id, u.first_name, u.last_name, u.username, u.email, u.password, u.is_active, u.created_at, u.updated_at, roles.* + FROM users u + JOIN roles ON u.role_id = roles.id + WHERE u.id = $1 AND u.is_active = true + `) + if err != nil { + return nil, err + } + + store.getByEmailStmt, err = db.Prepare(` + SELECT u.id, u.first_name, u.last_name, u.username, u.email, u.password, u.is_active, u.created_at, u.updated_at, roles.* + FROM users u + JOIN roles ON u.role_id = roles.id + WHERE u.email = $1 AND u.is_active = true + `) + if err != nil { + return nil, err + } + + store.getByInvitationStmt, err = db.Prepare(` + SELECT u.id, u.first_name, u.last_name, u.username, u.email, u.created_at, u.updated_at, is_active + FROM users u + JOIN invitations i ON u.id = i.user_id + WHERE i.token = $1 AND i.expiry > $2 + `) + if err != nil { + return nil, err + } + + store.updateStmt, err = db.Prepare(` + UPDATE users + SET first_name = $1, last_name = $2, username = $3, email = $4, password = $5 + WHERE id = $6 + `) + if err != nil { + return nil, err + } + + store.deleteStmt, err = db.Prepare(` + DELETE FROM users WHERE id = $1 + `) + if err != nil { + return nil, err + } + + store.invitationDeleteStmt, err = db.Prepare(` + DELETE FROM invitations WHERE user_id = $1 + `) + if err != nil { + return nil, err + } + + store.inviteCreateStmt, err = db.Prepare(` + INSERT INTO invitations (user_id, token, expiry) VALUES ($1, $2, $3) + `) + if err != nil { + return nil, err + } + + store.passwordResetRequestCreateStmt, err = db.Prepare(` + INSERT INTO password_reset_requests (user_id, token, expiry) VALUES ($1, $2, $3) + `) + if err != nil { + return nil, err + } + + return store, nil +} + +func (s *UserStore) Close() error { + var errs []error + if err := s.getByIDStmt.Close(); err != nil { + errs = append(errs, err) + } + if err := s.getByEmailStmt.Close(); err != nil { + errs = append(errs, err) + } + if err := s.getByInvitationStmt.Close(); err != nil { + errs = append(errs, err) + } + if err := s.updateStmt.Close(); err != nil { + errs = append(errs, err) + } + if err := s.deleteStmt.Close(); err != nil { + errs = append(errs, err) + } + if err := s.invitationDeleteStmt.Close(); err != nil { + errs = append(errs, err) + } + + if len(errs) > 0 { + return errs[0] + } + + return nil } func (s *UserStore) Create(ctx context.Context, tx *sql.Tx, user *User) error { - query := ` - INSERT INTO users (first_name, last_name, username, email, password, role_id) - VALUES ($1, $2, $3, $4, $5, (SELECT id FROM roles WHERE name = $6)) RETURNING id, created_at, updated_at - ` - ctx, cancel := context.WithTimeout(ctx, QueryTimeout) - defer cancel() role := user.Role.Name if role == "" { role = "user" } - err := tx.QueryRowContext( + query, _, err := psql.Insert("users"). + Columns("first_name", "last_name", "username", "email", "password", "role_id"). + Values(user.FirstName, user.LastName, user.Username, user.Email, user.Password.hash, sq.Expr("(SELECT id FROM roles WHERE name = ?)", role)). + Suffix("RETURNING id, created_at, updated_at").ToSql() + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(ctx, QueryTimeout) + defer cancel() + + err = tx.QueryRowContext( ctx, query, user.FirstName, @@ -81,28 +204,26 @@ func (s *UserStore) Create(ctx context.Context, tx *sql.Tx, user *User) error { return nil } +func (s *UserStore) GetAll(ctx context.Context, isAdmin bool) ([]User, error) { + +} + func (s *UserStore) GetByID(ctx context.Context, id int64) (*User, error) { - query := ` - SELECT users.id, first_name, last_name, username, email, password, created_at, updated_at, roles.* - FROM users - JOIN roles ON (users.role_id = roles.id) - WHERE users.id = $1 - AND is_active = true - ` ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() user := &User{} - err := s.db.QueryRowContext(ctx, query, id).Scan( + err := s.getByIDStmt.QueryRowContext(ctx, id).Scan( &user.ID, &user.FirstName, &user.LastName, &user.Username, &user.Email, &user.Password.hash, + &user.IsActive, &user.CreatedAt, &user.UpdatedAt, - &user.Role.ID, + &user.RoleID, &user.Role.Name, &user.Role.Level, &user.Role.Description, @@ -115,25 +236,16 @@ func (s *UserStore) GetByID(ctx context.Context, id int64) (*User, error) { return nil, err } } - return user, nil } func (s *UserStore) GetByEmail(ctx context.Context, email string) (*User, error) { - query := ` - SELECT users.id, first_name, last_name, username, email, password, created_at, updated_at, roles.* - FROM users - JOIN roles ON (users.role_id = roles.id) - WHERE email = $1 - AND is_active = true - ` - ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() user := &User{} - err := s.db.QueryRowContext(ctx, query, email).Scan( + err := s.getByEmailStmt.QueryRowContext(ctx, email).Scan( &user.ID, &user.FirstName, &user.LastName, @@ -142,7 +254,8 @@ func (s *UserStore) GetByEmail(ctx context.Context, email string) (*User, error) &user.Password.hash, &user.CreatedAt, &user.UpdatedAt, - &user.Role.ID, + &user.IsActive, + &user.RoleID, &user.Role.Name, &user.Role.Level, &user.Role.Description, @@ -160,17 +273,11 @@ func (s *UserStore) GetByEmail(ctx context.Context, email string) (*User, error) } func (s *UserStore) Update(ctx context.Context, user *User) error { - query := ` - UPDATE users - SET first_name = $1, last_name = $2, username = $3, email = $4, password = $5 - WHERE id = $6 - ` ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() - _, err := s.db.ExecContext( + _, err := s.updateStmt.ExecContext( ctx, - query, user.FirstName, user.LastName, user.Username, @@ -199,10 +306,20 @@ func (s *UserStore) CreateAndInvite(ctx context.Context, user *User, token strin }) } +func (s *UserStore) PasswordResetRequest(ctx context.Context, userID int64, token string, expiration time.Duration) error { + return withTx(s.db, ctx, func(tx *sql.Tx) error { + if err := s.createPasswordResetRequest(ctx, tx, userID, token, expiration); err != nil { + return err + } + + return nil + }) +} + func (s *UserStore) Activate(ctx context.Context, token string) error { return withTx(s.db, ctx, func(tx *sql.Tx) error { // Find the user that this token belongs to - user, err := getUserFromInvitation(ctx, tx, token) + user, err := s.getUserFromInvitation(ctx, tx, token) if err != nil { return err } @@ -238,12 +355,18 @@ func (s *UserStore) Delete(ctx context.Context, id int64) error { // * Soft delete user func (s *UserStore) DeleteUser(ctx context.Context, id int64) error { - query := `UPDATE users SET is_active = false WHERE id = $1` + query, _, err := psql.Update("users"). + Set("is_active", false). + Where(sq.Eq{"id": id}). + ToSql() + if err != nil { + return err + } ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() - _, err := s.db.ExecContext(ctx, query, id) + _, err = s.db.ExecContext(ctx, query, id) if err != nil { return err } @@ -255,22 +378,18 @@ func (p *password) Compare(password string) error { return bcrypt.CompareHashAndPassword(p.hash, []byte(password)) } -func getUserFromInvitation(ctx context.Context, tx *sql.Tx, token string) (*User, error) { - query := ` - SELECT u.id, u.first_name, u.last_name, u.username, u.email, u.created_at, u.updated_at, is_active - FROM users u - JOIN invitations i ON u.id = i.user_id - WHERE i.token = $1 AND i.expiry > $2 - ` - +func (s *UserStore) getUserFromInvitation(ctx context.Context, tx *sql.Tx, token string) (*User, error) { hashed := sha256.Sum256([]byte(token)) hashedToken := hex.EncodeToString(hashed[:]) ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() + stmt := tx.Stmt(s.getByInvitationStmt) + defer stmt.Close() + user := &User{} - err := tx.QueryRowContext(ctx, query, hashedToken, time.Now()).Scan( + err := stmt.QueryRowContext(ctx, hashedToken, time.Now()).Scan( &user.ID, &user.FirstName, &user.LastName, @@ -293,14 +412,28 @@ func getUserFromInvitation(ctx context.Context, tx *sql.Tx, token string) (*User } func (s *UserStore) createUserInvitation(ctx context.Context, tx *sql.Tx, userID int64, exp time.Duration, token string) error { - query := ` - INSERT INTO invitations (user_id, token, expiry) - VALUES ($1, $2, $3) - ` ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() - _, err := tx.ExecContext(ctx, query, userID, token, time.Now().Add(exp)) + stmt := tx.Stmt(s.inviteCreateStmt) + defer stmt.Close() + + _, err := stmt.ExecContext(ctx, userID, token, time.Now().Add(exp)) + if err != nil { + return err + } + + return nil +} + +func (s *UserStore) createPasswordResetRequest(ctx context.Context, tx *sql.Tx, userID int64, token string, expiry time.Duration) error { + ctx, cancel := context.WithTimeout(ctx, QueryTimeout) + defer cancel() + + stmt := tx.Stmt(s.passwordResetRequestCreateStmt) + defer stmt.Close() + + _, err := stmt.ExecContext(ctx, userID, token, time.Now().Add(expiry)) if err != nil { return err } @@ -309,17 +442,14 @@ func (s *UserStore) createUserInvitation(ctx context.Context, tx *sql.Tx, userID } func (s *UserStore) update(ctx context.Context, tx *sql.Tx, user *User) error { - query := ` - UPDATE users - SET first_name = $1, last_name = $2, username = $3, email = $4, is_active = $5 - WHERE id = $6 - ` ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() - _, err := tx.ExecContext( + stmt := tx.Stmt(s.updateStmt) + defer stmt.Close() + + _, err := stmt.ExecContext( ctx, - query, user.FirstName, user.LastName, user.Username, @@ -335,12 +465,13 @@ func (s *UserStore) update(ctx context.Context, tx *sql.Tx, user *User) error { } func (s *UserStore) deleteUserInvitation(ctx context.Context, tx *sql.Tx, userID int64) error { - query := `DELETE FROM invitations WHERE user_id = $1` - ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() - _, err := tx.ExecContext(ctx, query, userID) + stmt := tx.Stmt(s.invitationDeleteStmt) + defer stmt.Close() + + _, err := stmt.ExecContext(ctx, userID) if err != nil { return err } @@ -349,12 +480,13 @@ func (s *UserStore) deleteUserInvitation(ctx context.Context, tx *sql.Tx, userID } func (s *UserStore) delete(ctx context.Context, tx *sql.Tx, id int64) error { - query := `DELETE FROM users WHERE id = $1` - ctx, cancel := context.WithTimeout(ctx, QueryTimeout) defer cancel() - _, err := tx.ExecContext(ctx, query, id) + stmt := tx.Stmt(s.deleteStmt) + defer stmt.Close() + + _, err := stmt.ExecContext(ctx, id) if err != nil { return err }