first commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
.env
|
||||||
|
.idea
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
module ristek-task-be
|
||||||
|
|
||||||
|
go 1.26.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/jackc/pgx/v5 v5.8.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/lestrrat-go/jwx/v3 v3.0.13
|
||||||
|
golang.org/x/crypto v0.48.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.3 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||||
|
github.com/lestrrat-go/dsig v1.0.0 // indirect
|
||||||
|
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
|
github.com/lestrrat-go/httprc/v3 v3.0.2 // indirect
|
||||||
|
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
|
||||||
|
github.com/segmentio/asm v1.2.1 // indirect
|
||||||
|
github.com/valyala/fastjson v1.6.7 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||||
|
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||||
|
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||||
|
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
||||||
|
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
|
||||||
|
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
|
||||||
|
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
|
||||||
|
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||||
|
github.com/lestrrat-go/httprc/v3 v3.0.2 h1:7u4HUaD0NQbf2/n5+fyp+T10hNCsAnwKfqn4A4Baif0=
|
||||||
|
github.com/lestrrat-go/httprc/v3 v3.0.2/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
|
||||||
|
github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk=
|
||||||
|
github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU=
|
||||||
|
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
|
||||||
|
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||||
|
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
|
||||||
|
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_refresh_tokens_user_id;
|
||||||
|
DROP TABLE IF EXISTS refresh_tokens;
|
||||||
|
DROP TABLE IF EXISTS users;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||||
|
|
||||||
|
CREATE TABLE users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE refresh_tokens (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
token TEXT NOT NULL UNIQUE,
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_question_options_question;
|
||||||
|
DROP INDEX IF EXISTS idx_questions_form_id;
|
||||||
|
DROP INDEX IF EXISTS idx_forms_user_id;
|
||||||
|
DROP TABLE IF EXISTS question_options;
|
||||||
|
DROP TABLE IF EXISTS questions;
|
||||||
|
DROP TABLE IF EXISTS forms;
|
||||||
|
DROP TYPE IF EXISTS question_type;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TYPE question_type AS ENUM (
|
||||||
|
'short_text',
|
||||||
|
'long_text',
|
||||||
|
'multiple_choice',
|
||||||
|
'checkbox',
|
||||||
|
'dropdown',
|
||||||
|
'date',
|
||||||
|
'rating'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE forms (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
response_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE questions (
|
||||||
|
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
form_id UUID NOT NULL REFERENCES forms(id) ON DELETE CASCADE,
|
||||||
|
type question_type NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
required BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
position INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (form_id, id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE question_options (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
form_id UUID NOT NULL,
|
||||||
|
question_id UUID NOT NULL,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
position INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (form_id, question_id)
|
||||||
|
REFERENCES questions(form_id, id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_forms_user_id ON forms(user_id);
|
||||||
|
CREATE INDEX idx_questions_form_id ON questions(form_id);
|
||||||
|
CREATE INDEX idx_question_options_question ON question_options(form_id, question_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
-- name: CreateUser :exec
|
||||||
|
INSERT INTO users (email, password_hash)
|
||||||
|
VALUES ($1, $2);
|
||||||
|
|
||||||
|
-- name: GetUserByEmail :one
|
||||||
|
SELECT * FROM users
|
||||||
|
WHERE email = $1
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- name: GetUserByID :one
|
||||||
|
SELECT * FROM users
|
||||||
|
WHERE id = $1
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- name: UpdateUserPassword :one
|
||||||
|
UPDATE users
|
||||||
|
SET password_hash = $2,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: CreateRefreshToken :one
|
||||||
|
INSERT INTO refresh_tokens (user_id, token, expires_at)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetRefreshToken :one
|
||||||
|
SELECT * FROM refresh_tokens
|
||||||
|
WHERE token = $1
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- name: DeleteRefreshToken :exec
|
||||||
|
DELETE FROM refresh_tokens
|
||||||
|
WHERE token = $1;
|
||||||
|
|
||||||
|
-- name: DeleteUserRefreshTokens :exec
|
||||||
|
DELETE FROM refresh_tokens
|
||||||
|
WHERE user_id = $1;
|
||||||
|
|
||||||
|
-- name: DeleteExpiredRefreshTokens :exec
|
||||||
|
DELETE FROM refresh_tokens
|
||||||
|
WHERE expires_at < now();
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- name: ListForms :many
|
||||||
|
SELECT * FROM forms
|
||||||
|
WHERE user_id = $1
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
|
||||||
|
-- name: GetFormByID :one
|
||||||
|
SELECT * FROM forms
|
||||||
|
WHERE id = $1
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- name: CreateForm :one
|
||||||
|
INSERT INTO forms (user_id, title, description)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: UpdateForm :one
|
||||||
|
UPDATE forms
|
||||||
|
SET title = $2,
|
||||||
|
description = $3,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: DeleteForm :exec
|
||||||
|
DELETE FROM forms
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: IncrementResponseCount :one
|
||||||
|
UPDATE forms
|
||||||
|
SET response_count = response_count + 1,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- name: CreateQuestionOption :one
|
||||||
|
INSERT INTO question_options (form_id, question_id, label, position)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetOptionsByFormID :many
|
||||||
|
SELECT * FROM question_options
|
||||||
|
WHERE form_id = $1
|
||||||
|
ORDER BY question_id, position ASC;
|
||||||
|
|
||||||
|
-- name: GetOptionsByQuestionID :many
|
||||||
|
SELECT * FROM question_options
|
||||||
|
WHERE form_id = $1 AND question_id = $2
|
||||||
|
ORDER BY position ASC;
|
||||||
|
|
||||||
|
-- name: DeleteOptionsByFormID :exec
|
||||||
|
DELETE FROM question_options
|
||||||
|
WHERE form_id = $1;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- name: CreateQuestion :one
|
||||||
|
INSERT INTO questions (form_id, type, title, required, position)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetQuestionsByFormID :many
|
||||||
|
SELECT * FROM questions
|
||||||
|
WHERE form_id = $1
|
||||||
|
ORDER BY position ASC;
|
||||||
|
|
||||||
|
-- name: DeleteQuestionsByFormID :exec
|
||||||
|
DELETE FROM questions
|
||||||
|
WHERE form_id = $1;
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: auth.sql
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createRefreshToken = `-- name: CreateRefreshToken :one
|
||||||
|
INSERT INTO refresh_tokens (user_id, token, expires_at)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id, user_id, token, expires_at, created_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateRefreshTokenParams struct {
|
||||||
|
UserID uuid.UUID
|
||||||
|
Token string
|
||||||
|
ExpiresAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createRefreshToken, arg.UserID, arg.Token, arg.ExpiresAt)
|
||||||
|
var i RefreshToken
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Token,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createUser = `-- name: CreateUser :exec
|
||||||
|
INSERT INTO users (email, password_hash)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateUserParams struct {
|
||||||
|
Email string
|
||||||
|
PasswordHash string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, createUser, arg.Email, arg.PasswordHash)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteExpiredRefreshTokens = `-- name: DeleteExpiredRefreshTokens :exec
|
||||||
|
DELETE FROM refresh_tokens
|
||||||
|
WHERE expires_at < now()
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteExpiredRefreshTokens(ctx context.Context) error {
|
||||||
|
_, err := q.db.Exec(ctx, deleteExpiredRefreshTokens)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteRefreshToken = `-- name: DeleteRefreshToken :exec
|
||||||
|
DELETE FROM refresh_tokens
|
||||||
|
WHERE token = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteRefreshToken(ctx context.Context, token string) error {
|
||||||
|
_, err := q.db.Exec(ctx, deleteRefreshToken, token)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteUserRefreshTokens = `-- name: DeleteUserRefreshTokens :exec
|
||||||
|
DELETE FROM refresh_tokens
|
||||||
|
WHERE user_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteUserRefreshTokens(ctx context.Context, userID uuid.UUID) error {
|
||||||
|
_, err := q.db.Exec(ctx, deleteUserRefreshTokens, userID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRefreshToken = `-- name: GetRefreshToken :one
|
||||||
|
SELECT id, user_id, token, expires_at, created_at FROM refresh_tokens
|
||||||
|
WHERE token = $1
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetRefreshToken(ctx context.Context, token string) (RefreshToken, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getRefreshToken, token)
|
||||||
|
var i RefreshToken
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Token,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||||
|
SELECT id, email, password_hash, created_at, updated_at FROM users
|
||||||
|
WHERE email = $1
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getUserByEmail, email)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Email,
|
||||||
|
&i.PasswordHash,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserByID = `-- name: GetUserByID :one
|
||||||
|
SELECT id, email, password_hash, created_at, updated_at FROM users
|
||||||
|
WHERE id = $1
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getUserByID, id)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Email,
|
||||||
|
&i.PasswordHash,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUserPassword = `-- name: UpdateUserPassword :one
|
||||||
|
UPDATE users
|
||||||
|
SET password_hash = $2,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, email, password_hash, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateUserPasswordParams struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
PasswordHash string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) (User, error) {
|
||||||
|
row := q.db.QueryRow(ctx, updateUserPassword, arg.ID, arg.PasswordHash)
|
||||||
|
var i User
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Email,
|
||||||
|
&i.PasswordHash,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DBTX interface {
|
||||||
|
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||||
|
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||||
|
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db DBTX) *Queries {
|
||||||
|
return &Queries{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queries struct {
|
||||||
|
db DBTX
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||||
|
return &Queries{
|
||||||
|
db: tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: forms.sql
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createForm = `-- name: CreateForm :one
|
||||||
|
INSERT INTO forms (user_id, title, description)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id, user_id, title, description, response_count, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateFormParams struct {
|
||||||
|
UserID uuid.UUID
|
||||||
|
Title string
|
||||||
|
Description pgtype.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateForm(ctx context.Context, arg CreateFormParams) (Form, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createForm, arg.UserID, arg.Title, arg.Description)
|
||||||
|
var i Form
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.ResponseCount,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteForm = `-- name: DeleteForm :exec
|
||||||
|
DELETE FROM forms
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteForm(ctx context.Context, id uuid.UUID) error {
|
||||||
|
_, err := q.db.Exec(ctx, deleteForm, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFormByID = `-- name: GetFormByID :one
|
||||||
|
SELECT id, user_id, title, description, response_count, created_at, updated_at FROM forms
|
||||||
|
WHERE id = $1
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetFormByID(ctx context.Context, id uuid.UUID) (Form, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getFormByID, id)
|
||||||
|
var i Form
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.ResponseCount,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const incrementResponseCount = `-- name: IncrementResponseCount :one
|
||||||
|
UPDATE forms
|
||||||
|
SET response_count = response_count + 1,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, user_id, title, description, response_count, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) IncrementResponseCount(ctx context.Context, id uuid.UUID) (Form, error) {
|
||||||
|
row := q.db.QueryRow(ctx, incrementResponseCount, id)
|
||||||
|
var i Form
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.ResponseCount,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listForms = `-- name: ListForms :many
|
||||||
|
SELECT id, user_id, title, description, response_count, created_at, updated_at FROM forms
|
||||||
|
WHERE user_id = $1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListForms(ctx context.Context, userID uuid.UUID) ([]Form, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listForms, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Form
|
||||||
|
for rows.Next() {
|
||||||
|
var i Form
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.ResponseCount,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateForm = `-- name: UpdateForm :one
|
||||||
|
UPDATE forms
|
||||||
|
SET title = $2,
|
||||||
|
description = $3,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, user_id, title, description, response_count, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateFormParams struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Title string
|
||||||
|
Description pgtype.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateForm(ctx context.Context, arg UpdateFormParams) (Form, error) {
|
||||||
|
row := q.db.QueryRow(ctx, updateForm, arg.ID, arg.Title, arg.Description)
|
||||||
|
var i Form
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.ResponseCount,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QuestionType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
QuestionTypeShortText QuestionType = "short_text"
|
||||||
|
QuestionTypeLongText QuestionType = "long_text"
|
||||||
|
QuestionTypeMultipleChoice QuestionType = "multiple_choice"
|
||||||
|
QuestionTypeCheckbox QuestionType = "checkbox"
|
||||||
|
QuestionTypeDropdown QuestionType = "dropdown"
|
||||||
|
QuestionTypeDate QuestionType = "date"
|
||||||
|
QuestionTypeRating QuestionType = "rating"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *QuestionType) Scan(src interface{}) error {
|
||||||
|
switch s := src.(type) {
|
||||||
|
case []byte:
|
||||||
|
*e = QuestionType(s)
|
||||||
|
case string:
|
||||||
|
*e = QuestionType(s)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported scan type for QuestionType: %T", src)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NullQuestionType struct {
|
||||||
|
QuestionType QuestionType
|
||||||
|
Valid bool // Valid is true if QuestionType is not NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (ns *NullQuestionType) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
ns.QuestionType, ns.Valid = "", false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ns.Valid = true
|
||||||
|
return ns.QuestionType.Scan(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (ns NullQuestionType) Value() (driver.Value, error) {
|
||||||
|
if !ns.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return string(ns.QuestionType), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Form struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
UserID uuid.UUID
|
||||||
|
Title string
|
||||||
|
Description pgtype.Text
|
||||||
|
ResponseCount int32
|
||||||
|
CreatedAt pgtype.Timestamptz
|
||||||
|
UpdatedAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
type Question struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
FormID uuid.UUID
|
||||||
|
Type QuestionType
|
||||||
|
Title string
|
||||||
|
Required bool
|
||||||
|
Position int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuestionOption struct {
|
||||||
|
ID int32
|
||||||
|
FormID uuid.UUID
|
||||||
|
QuestionID uuid.UUID
|
||||||
|
Label string
|
||||||
|
Position int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshToken struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
UserID uuid.UUID
|
||||||
|
Token string
|
||||||
|
ExpiresAt pgtype.Timestamptz
|
||||||
|
CreatedAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Email string
|
||||||
|
PasswordHash string
|
||||||
|
CreatedAt pgtype.Timestamptz
|
||||||
|
UpdatedAt pgtype.Timestamptz
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: question_options.sql
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createQuestionOption = `-- name: CreateQuestionOption :one
|
||||||
|
INSERT INTO question_options (form_id, question_id, label, position)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, form_id, question_id, label, position
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateQuestionOptionParams struct {
|
||||||
|
FormID uuid.UUID
|
||||||
|
QuestionID uuid.UUID
|
||||||
|
Label string
|
||||||
|
Position int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateQuestionOption(ctx context.Context, arg CreateQuestionOptionParams) (QuestionOption, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createQuestionOption,
|
||||||
|
arg.FormID,
|
||||||
|
arg.QuestionID,
|
||||||
|
arg.Label,
|
||||||
|
arg.Position,
|
||||||
|
)
|
||||||
|
var i QuestionOption
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.FormID,
|
||||||
|
&i.QuestionID,
|
||||||
|
&i.Label,
|
||||||
|
&i.Position,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteOptionsByFormID = `-- name: DeleteOptionsByFormID :exec
|
||||||
|
DELETE FROM question_options
|
||||||
|
WHERE form_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteOptionsByFormID(ctx context.Context, formID uuid.UUID) error {
|
||||||
|
_, err := q.db.Exec(ctx, deleteOptionsByFormID, formID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOptionsByFormID = `-- name: GetOptionsByFormID :many
|
||||||
|
SELECT id, form_id, question_id, label, position FROM question_options
|
||||||
|
WHERE form_id = $1
|
||||||
|
ORDER BY question_id, position ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetOptionsByFormID(ctx context.Context, formID uuid.UUID) ([]QuestionOption, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getOptionsByFormID, formID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []QuestionOption
|
||||||
|
for rows.Next() {
|
||||||
|
var i QuestionOption
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.FormID,
|
||||||
|
&i.QuestionID,
|
||||||
|
&i.Label,
|
||||||
|
&i.Position,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOptionsByQuestionID = `-- name: GetOptionsByQuestionID :many
|
||||||
|
SELECT id, form_id, question_id, label, position FROM question_options
|
||||||
|
WHERE form_id = $1 AND question_id = $2
|
||||||
|
ORDER BY position ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetOptionsByQuestionIDParams struct {
|
||||||
|
FormID uuid.UUID
|
||||||
|
QuestionID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetOptionsByQuestionID(ctx context.Context, arg GetOptionsByQuestionIDParams) ([]QuestionOption, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getOptionsByQuestionID, arg.FormID, arg.QuestionID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []QuestionOption
|
||||||
|
for rows.Next() {
|
||||||
|
var i QuestionOption
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.FormID,
|
||||||
|
&i.QuestionID,
|
||||||
|
&i.Label,
|
||||||
|
&i.Position,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: questions.sql
|
||||||
|
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createQuestion = `-- name: CreateQuestion :one
|
||||||
|
INSERT INTO questions (form_id, type, title, required, position)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
RETURNING id, form_id, type, title, required, position
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateQuestionParams struct {
|
||||||
|
FormID uuid.UUID
|
||||||
|
Type QuestionType
|
||||||
|
Title string
|
||||||
|
Required bool
|
||||||
|
Position int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams) (Question, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createQuestion,
|
||||||
|
arg.FormID,
|
||||||
|
arg.Type,
|
||||||
|
arg.Title,
|
||||||
|
arg.Required,
|
||||||
|
arg.Position,
|
||||||
|
)
|
||||||
|
var i Question
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.FormID,
|
||||||
|
&i.Type,
|
||||||
|
&i.Title,
|
||||||
|
&i.Required,
|
||||||
|
&i.Position,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteQuestionsByFormID = `-- name: DeleteQuestionsByFormID :exec
|
||||||
|
DELETE FROM questions
|
||||||
|
WHERE form_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteQuestionsByFormID(ctx context.Context, formID uuid.UUID) error {
|
||||||
|
_, err := q.db.Exec(ctx, deleteQuestionsByFormID, formID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getQuestionsByFormID = `-- name: GetQuestionsByFormID :many
|
||||||
|
SELECT id, form_id, type, title, required, position FROM questions
|
||||||
|
WHERE form_id = $1
|
||||||
|
ORDER BY position ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetQuestionsByFormID(ctx context.Context, formID uuid.UUID) ([]Question, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getQuestionsByFormID, formID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Question
|
||||||
|
for rows.Next() {
|
||||||
|
var i Question
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.FormID,
|
||||||
|
&i.Type,
|
||||||
|
&i.Title,
|
||||||
|
&i.Required,
|
||||||
|
&i.Position,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
version: "2"
|
||||||
|
sql:
|
||||||
|
- engine: "postgresql"
|
||||||
|
queries: "queries"
|
||||||
|
schema: "migrations"
|
||||||
|
gen:
|
||||||
|
go:
|
||||||
|
package: "repository"
|
||||||
|
out: "repository"
|
||||||
|
sql_package: "pgx/v5"
|
||||||
|
overrides:
|
||||||
|
- db_type: "uuid"
|
||||||
|
go_type:
|
||||||
|
import: "github.com/google/uuid"
|
||||||
|
type: "UUID"
|
||||||
|
- db_type: "uuid"
|
||||||
|
go_type:
|
||||||
|
import: "github.com/google/uuid"
|
||||||
|
type: "UUID"
|
||||||
|
pointer: true
|
||||||
|
nullable: true
|
||||||
|
- db_type: "timestamptz"
|
||||||
|
go_type:
|
||||||
|
type: "*time.Time"
|
||||||
|
pointer: true
|
||||||
|
nullable: true
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"ristek-task-be/internal/db/sqlc/repository"
|
||||||
|
"ristek-task-be/internal/middleware"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Auth struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDuplicateError(err error) bool {
|
||||||
|
var pgErr *pgconn.PgError
|
||||||
|
if errors.As(err, &pgErr) {
|
||||||
|
return pgErr.Code == "23505"
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) RegisterPost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var register Auth
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(®ister); err != nil {
|
||||||
|
badRequest(w, err)
|
||||||
|
log.Printf("failed to decode request body: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if register.Email == "" || register.Password == "" {
|
||||||
|
badRequest(w, errors.New("email and password are required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword(
|
||||||
|
[]byte(register.Password),
|
||||||
|
bcrypt.DefaultCost,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to hash password: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = h.repository.CreateUser(ctx, repository.CreateUserParams{
|
||||||
|
Email: register.Email,
|
||||||
|
PasswordHash: string(hashedPassword),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if isDuplicateError(err) {
|
||||||
|
badRequest(w, errors.New("email already exists"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to create user: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshRequest struct {
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) RefreshPost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req RefreshRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
badRequest(w, err)
|
||||||
|
log.Printf("failed to decode request body: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.RefreshToken == "" {
|
||||||
|
badRequest(w, errors.New("refresh_token is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
rt, err := h.repository.GetRefreshToken(ctx, req.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to get refresh token: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rt.ExpiresAt.Time.Before(time.Now()) {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.repository.GetUserByID(ctx, rt.UserID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to get user: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.repository.DeleteRefreshToken(ctx, req.RefreshToken); err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to delete old refresh token: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken, err := h.jwt.GenerateAccessToken(user.ID.String(), user.Email)
|
||||||
|
if err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to generate access token: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawRefreshToken := uuid.New().String()
|
||||||
|
_, err = h.repository.CreateRefreshToken(ctx, repository.CreateRefreshTokenParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
Token: rawRefreshToken,
|
||||||
|
ExpiresAt: pgtype.Timestamptz{Time: time.Now().Add(7 * 24 * time.Hour), Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to create refresh token: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"access_token": accessToken,
|
||||||
|
"refresh_token": rawRefreshToken,
|
||||||
|
"expires_in": 900,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogoutRequest struct {
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) LogoutPost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req LogoutRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
badRequest(w, err)
|
||||||
|
log.Printf("failed to decode request body: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.RefreshToken == "" {
|
||||||
|
badRequest(w, errors.New("refresh_token is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := h.repository.DeleteRefreshToken(ctx, req.RefreshToken); err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to delete refresh token: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) LogoutAllDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userIDStr, ok := r.Context().Value(middleware.UserIDKey).(string)
|
||||||
|
if !ok || userIDStr == "" {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := uuid.Parse(userIDStr)
|
||||||
|
if err != nil {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := h.repository.DeleteUserRefreshTokens(ctx, userID); err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to delete user refresh tokens: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) MeGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userIDStr, ok := r.Context().Value(middleware.UserIDKey).(string)
|
||||||
|
if !ok || userIDStr == "" {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := uuid.Parse(userIDStr)
|
||||||
|
if err != nil {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
user, err := h.repository.GetUserByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to get user: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"id": user.ID.String(),
|
||||||
|
"email": user.Email,
|
||||||
|
"created_at": user.CreatedAt.Time,
|
||||||
|
"updated_at": user.UpdatedAt.Time,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangePasswordRequest struct {
|
||||||
|
OldPassword string `json:"old_password"`
|
||||||
|
NewPassword string `json:"new_password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) MePasswordPatch(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userIDStr, ok := r.Context().Value(middleware.UserIDKey).(string)
|
||||||
|
if !ok || userIDStr == "" {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := uuid.Parse(userIDStr)
|
||||||
|
if err != nil {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req ChangePasswordRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
badRequest(w, err)
|
||||||
|
log.Printf("failed to decode request body: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.OldPassword == "" || req.NewPassword == "" {
|
||||||
|
badRequest(w, errors.New("old_password and new_password are required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
user, err := h.repository.GetUserByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to get user: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.OldPassword)); err != nil {
|
||||||
|
badRequest(w, errors.New("incorrect old password"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to hash password: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = h.repository.UpdateUserPassword(ctx, repository.UpdateUserPasswordParams{
|
||||||
|
ID: userID,
|
||||||
|
PasswordHash: string(hashedPassword),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to update password: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) LoginPost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var login Auth
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&login); err != nil {
|
||||||
|
badRequest(w, err)
|
||||||
|
log.Printf("failed to decode request body: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if login.Email == "" || login.Password == "" {
|
||||||
|
badRequest(w, errors.New("email and password are required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.repository.GetUserByEmail(r.Context(), login.Email)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to get user by email: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword(
|
||||||
|
[]byte(user.PasswordHash),
|
||||||
|
[]byte(login.Password),
|
||||||
|
); err != nil {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken, err := h.jwt.GenerateAccessToken(user.ID.String(), user.Email)
|
||||||
|
if err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to generate access token: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawRefreshToken := uuid.New().String()
|
||||||
|
_, err = h.repository.CreateRefreshToken(r.Context(), repository.CreateRefreshTokenParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
Token: rawRefreshToken,
|
||||||
|
ExpiresAt: pgtype.Timestamptz{Time: time.Now().Add(7 * 24 * time.Hour), Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to create refresh token: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"access_token": accessToken,
|
||||||
|
"refresh_token": rawRefreshToken,
|
||||||
|
"expires_in": 900,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,435 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"ristek-task-be/internal/db/sqlc/repository"
|
||||||
|
"ristek-task-be/internal/middleware"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QuestionOptionInput struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
Position int32 `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuestionInput struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
Position int32 `json:"position"`
|
||||||
|
Options []QuestionOptionInput `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateFormRequest struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Questions []QuestionInput `json:"questions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateFormRequest struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Questions []QuestionInput `json:"questions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuestionOptionResponse struct {
|
||||||
|
ID int32 `json:"id"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Position int32 `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuestionResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
Position int32 `json:"position"`
|
||||||
|
Options []QuestionOptionResponse `json:"options"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ResponseCount int32 `json:"response_count"`
|
||||||
|
Questions []QuestionResponse `json:"questions"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func optionsByQuestion(options []repository.QuestionOption, questionID uuid.UUID) []QuestionOptionResponse {
|
||||||
|
var result []QuestionOptionResponse
|
||||||
|
for _, o := range options {
|
||||||
|
if o.QuestionID == questionID {
|
||||||
|
result = append(result, QuestionOptionResponse{
|
||||||
|
ID: o.ID,
|
||||||
|
Label: o.Label,
|
||||||
|
Position: o.Position,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result == nil {
|
||||||
|
result = []QuestionOptionResponse{}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildFormResponse(form repository.Form, questions []repository.Question, options []repository.QuestionOption) FormResponse {
|
||||||
|
qs := make([]QuestionResponse, 0, len(questions))
|
||||||
|
for _, q := range questions {
|
||||||
|
qs = append(qs, QuestionResponse{
|
||||||
|
ID: q.ID.String(),
|
||||||
|
Type: string(q.Type),
|
||||||
|
Title: q.Title,
|
||||||
|
Required: q.Required,
|
||||||
|
Position: q.Position,
|
||||||
|
Options: optionsByQuestion(options, q.ID),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
desc := ""
|
||||||
|
if form.Description.Valid {
|
||||||
|
desc = form.Description.String
|
||||||
|
}
|
||||||
|
|
||||||
|
return FormResponse{
|
||||||
|
ID: form.ID.String(),
|
||||||
|
UserID: form.UserID.String(),
|
||||||
|
Title: form.Title,
|
||||||
|
Description: desc,
|
||||||
|
ResponseCount: form.ResponseCount,
|
||||||
|
Questions: qs,
|
||||||
|
CreatedAt: form.CreatedAt.Time,
|
||||||
|
UpdatedAt: form.UpdatedAt.Time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) currentUserID(r *http.Request) (uuid.UUID, bool) {
|
||||||
|
str, ok := r.Context().Value(middleware.UserIDKey).(string)
|
||||||
|
if !ok || str == "" {
|
||||||
|
return uuid.UUID{}, false
|
||||||
|
}
|
||||||
|
id, err := uuid.Parse(str)
|
||||||
|
if err != nil {
|
||||||
|
return uuid.UUID{}, false
|
||||||
|
}
|
||||||
|
return id, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isChoiceType(t repository.QuestionType) bool {
|
||||||
|
switch t {
|
||||||
|
case repository.QuestionTypeMultipleChoice,
|
||||||
|
repository.QuestionTypeCheckbox,
|
||||||
|
repository.QuestionTypeDropdown:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) FormsGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID, ok := h.currentUserID(r)
|
||||||
|
if !ok {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
forms, err := h.repository.ListForms(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to list forms: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type listItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ResponseCount int32 `json:"response_count"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]listItem, 0, len(forms))
|
||||||
|
for _, f := range forms {
|
||||||
|
desc := ""
|
||||||
|
if f.Description.Valid {
|
||||||
|
desc = f.Description.String
|
||||||
|
}
|
||||||
|
items = append(items, listItem{
|
||||||
|
ID: f.ID.String(),
|
||||||
|
Title: f.Title,
|
||||||
|
Description: desc,
|
||||||
|
ResponseCount: f.ResponseCount,
|
||||||
|
CreatedAt: f.CreatedAt.Time,
|
||||||
|
UpdatedAt: f.UpdatedAt.Time,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) FormsPost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID, ok := h.currentUserID(r)
|
||||||
|
if !ok {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req CreateFormRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
badRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Title == "" {
|
||||||
|
badRequest(w, errors.New("title is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
form, err := h.repository.CreateForm(ctx, repository.CreateFormParams{
|
||||||
|
UserID: userID,
|
||||||
|
Title: req.Title,
|
||||||
|
Description: pgtype.Text{
|
||||||
|
String: req.Description,
|
||||||
|
Valid: req.Description != "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to create form: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
questions, options, err := h.saveQuestions(ctx, form.ID, req.Questions)
|
||||||
|
if err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to save questions: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_ = json.NewEncoder(w).Encode(buildFormResponse(form, questions, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) FormGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
formID, err := uuid.Parse(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
badRequest(w, errors.New("invalid form id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
form, err := h.repository.GetFormByID(ctx, formID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to get form: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
questions, err := h.repository.GetQuestionsByFormID(ctx, formID)
|
||||||
|
if err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to get questions: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options, err := h.repository.GetOptionsByFormID(ctx, formID)
|
||||||
|
if err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to get options: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(buildFormResponse(form, questions, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) FormPut(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID, ok := h.currentUserID(r)
|
||||||
|
if !ok {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
formID, err := uuid.Parse(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
badRequest(w, errors.New("invalid form id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req UpdateFormRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
badRequest(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Title == "" {
|
||||||
|
badRequest(w, errors.New("title is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
form, err := h.repository.GetFormByID(ctx, formID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
internalServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.UserID != userID {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := h.repository.UpdateForm(ctx, repository.UpdateFormParams{
|
||||||
|
ID: formID,
|
||||||
|
Title: req.Title,
|
||||||
|
Description: pgtype.Text{
|
||||||
|
String: req.Description,
|
||||||
|
Valid: req.Description != "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to update form: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.repository.DeleteOptionsByFormID(ctx, formID); err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to delete options: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.repository.DeleteQuestionsByFormID(ctx, formID); err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to delete questions: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
questions, options, err := h.saveQuestions(ctx, formID, req.Questions)
|
||||||
|
if err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to save questions: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(buildFormResponse(updated, questions, options))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) FormDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID, ok := h.currentUserID(r)
|
||||||
|
if !ok {
|
||||||
|
unauthorized(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
formID, err := uuid.Parse(r.PathValue("id"))
|
||||||
|
if err != nil {
|
||||||
|
badRequest(w, errors.New("invalid form id"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
form, err := h.repository.GetFormByID(ctx, formID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
internalServerError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.UserID != userID {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.repository.DeleteForm(ctx, formID); err != nil {
|
||||||
|
internalServerError(w, err)
|
||||||
|
log.Printf("failed to delete form: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) saveQuestions(ctx context.Context, formID uuid.UUID, inputs []QuestionInput) ([]repository.Question, []repository.QuestionOption, error) {
|
||||||
|
var questions []repository.Question
|
||||||
|
var options []repository.QuestionOption
|
||||||
|
|
||||||
|
for i, qi := range inputs {
|
||||||
|
qt := repository.QuestionType(qi.Type)
|
||||||
|
pos := qi.Position
|
||||||
|
if pos == 0 {
|
||||||
|
pos = int32(i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
q, err := h.repository.CreateQuestion(ctx, repository.CreateQuestionParams{
|
||||||
|
FormID: formID,
|
||||||
|
Type: qt,
|
||||||
|
Title: qi.Title,
|
||||||
|
Required: qi.Required,
|
||||||
|
Position: pos,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
questions = append(questions, q)
|
||||||
|
|
||||||
|
if isChoiceType(qt) {
|
||||||
|
for j, oi := range qi.Options {
|
||||||
|
optPos := oi.Position
|
||||||
|
if optPos == 0 {
|
||||||
|
optPos = int32(j + 1)
|
||||||
|
}
|
||||||
|
opt, err := h.repository.CreateQuestionOption(ctx, repository.CreateQuestionOptionParams{
|
||||||
|
FormID: formID,
|
||||||
|
QuestionID: q.ID,
|
||||||
|
Label: oi.Label,
|
||||||
|
Position: optPos,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
options = append(options, opt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return questions, options, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"ristek-task-be/internal/db/sqlc/repository"
|
||||||
|
"ristek-task-be/internal/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
repository *repository.Queries
|
||||||
|
jwt *jwt.JWT
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(repository *repository.Queries, jwt *jwt.JWT) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
repository: repository,
|
||||||
|
jwt: jwt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func badRequest(w http.ResponseWriter, err error) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, _ = w.Write([]byte(err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func unauthorized(w http.ResponseWriter) {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func internalServerError(w http.ResponseWriter, err error) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, _ = w.Write([]byte(err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) HealthGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("OK"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||||
|
"github.com/lestrrat-go/jwx/v3/jwt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JWT struct {
|
||||||
|
secret []byte
|
||||||
|
accessTokenTTL time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(secret string) *JWT {
|
||||||
|
return &JWT{
|
||||||
|
secret: []byte(secret),
|
||||||
|
accessTokenTTL: 15 * time.Minute,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JWT) GenerateAccessToken(userID, email string) (string, error) {
|
||||||
|
tok, err := jwt.NewBuilder().
|
||||||
|
Subject(userID).
|
||||||
|
IssuedAt(time.Now()).
|
||||||
|
Expiration(time.Now().Add(j.accessTokenTTL)).
|
||||||
|
Claim("type", "access").
|
||||||
|
Claim("email", email).
|
||||||
|
Build()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
signed, err := jwt.Sign(tok, jwt.WithKey(jwa.HS256(), j.secret))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(signed), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JWT) ValidateAccessToken(tokenStr string) (string, error) {
|
||||||
|
tok, err := jwt.Parse(
|
||||||
|
[]byte(tokenStr),
|
||||||
|
jwt.WithKey(jwa.HS256(), j.secret),
|
||||||
|
jwt.WithValidate(true),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
subject, ok := tok.Subject()
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New("token does not contain subject")
|
||||||
|
}
|
||||||
|
|
||||||
|
return subject, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"ristek-task-be/internal/jwt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wrapper struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
request *http.Request
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrapper) WriteHeader(code int) {
|
||||||
|
w.statusCode = code
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClientIP(request *http.Request) string {
|
||||||
|
ip := request.Header.Get("Cf-Connecting-IP")
|
||||||
|
if ip != "" {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
ip = request.Header.Get("X-Real-IP")
|
||||||
|
if ip == "" {
|
||||||
|
ip = request.Header.Get("X-Forwarded-For")
|
||||||
|
if ip == "" {
|
||||||
|
ip = request.RemoteAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(ip, ",") {
|
||||||
|
ips := strings.Split(ip, ",")
|
||||||
|
ip = strings.TrimSpace(ips[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(ip, ":") {
|
||||||
|
ips := strings.Split(ip, ":")
|
||||||
|
ip = ips[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func Handler(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
wrappedWriter := &wrapper{
|
||||||
|
ResponseWriter: writer,
|
||||||
|
request: request,
|
||||||
|
statusCode: http.StatusOK,
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(wrappedWriter, request)
|
||||||
|
log.Printf("%s %s %s %v", ClientIP(request), request.Method, request.RequestURI, wrappedWriter.statusCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const UserIDKey contextKey = "user_id"
|
||||||
|
|
||||||
|
func CORS(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||||
|
w.Header().Set("Access-Control-Max-Age", "86400")
|
||||||
|
|
||||||
|
if r.Method == http.MethodOptions {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Auth(j *jwt.JWT) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
|
|
||||||
|
userID, err := j.ValidateAccessToken(tokenStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(r.Context(), UserIDKey, userID)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"ristek-task-be/internal/db/sqlc/repository"
|
||||||
|
"ristek-task-be/internal/handler"
|
||||||
|
"ristek-task-be/internal/jwt"
|
||||||
|
"ristek-task-be/internal/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
addr string
|
||||||
|
port uint16
|
||||||
|
repository *repository.Queries
|
||||||
|
jwt *jwt.JWT
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(addr string, port uint16, repository *repository.Queries, jwt *jwt.JWT) *Server {
|
||||||
|
return &Server{
|
||||||
|
addr: addr,
|
||||||
|
port: port,
|
||||||
|
repository: repository,
|
||||||
|
jwt: jwt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func router(repository *repository.Queries, jwt *jwt.JWT) *http.ServeMux {
|
||||||
|
r := http.NewServeMux()
|
||||||
|
h := handler.New(repository, jwt)
|
||||||
|
|
||||||
|
r.HandleFunc("GET /health", h.HealthGet)
|
||||||
|
|
||||||
|
authRoute := http.NewServeMux()
|
||||||
|
r.Handle("/api/auth/", http.StripPrefix("/api/auth", authRoute))
|
||||||
|
authRoute.HandleFunc("POST /register", h.RegisterPost)
|
||||||
|
authRoute.HandleFunc("POST /login", h.LoginPost)
|
||||||
|
authRoute.HandleFunc("POST /refresh", h.RefreshPost)
|
||||||
|
authRoute.HandleFunc("POST /logout", h.LogoutPost)
|
||||||
|
authRoute.Handle("DELETE /logout/all", middleware.Auth(jwt)(http.HandlerFunc(h.LogoutAllDelete)))
|
||||||
|
authRoute.Handle("GET /me", middleware.Auth(jwt)(http.HandlerFunc(h.MeGet)))
|
||||||
|
authRoute.Handle("PATCH /me/password", middleware.Auth(jwt)(http.HandlerFunc(h.MePasswordPatch)))
|
||||||
|
|
||||||
|
formRoute := http.NewServeMux()
|
||||||
|
r.Handle("/api/forms", middleware.Auth(jwt)(http.HandlerFunc(h.FormsGet)))
|
||||||
|
r.Handle("/api/form/", http.StripPrefix("/api/form", formRoute))
|
||||||
|
formRoute.HandleFunc("GET /{id}", h.FormGet)
|
||||||
|
formRoute.Handle("POST /{$}", middleware.Auth(jwt)(http.HandlerFunc(h.FormsPost)))
|
||||||
|
formRoute.Handle("PUT /{id}", middleware.Auth(jwt)(http.HandlerFunc(h.FormPut)))
|
||||||
|
formRoute.Handle("DELETE /{id}", middleware.Auth(jwt)(http.HandlerFunc(h.FormDelete)))
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
r := router(s.repository, s.jwt)
|
||||||
|
hs := &http.Server{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", s.addr, s.port),
|
||||||
|
Handler: middleware.Handler(middleware.CORS(r)),
|
||||||
|
}
|
||||||
|
|
||||||
|
return hs.ListenAndServe()
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"ristek-task-be/internal/db/sqlc/repository"
|
||||||
|
"ristek-task-be/internal/jwt"
|
||||||
|
"ristek-task-be/internal/server"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetOutput(os.Stdout)
|
||||||
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
|
||||||
|
if _, err := os.Stat(".env"); err == nil {
|
||||||
|
if err = godotenv.Load(".env"); err != nil {
|
||||||
|
log.Printf("Warning: Failed to load .env file: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
signalChan := make(chan os.Signal, 1)
|
||||||
|
|
||||||
|
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
addr := "localhost"
|
||||||
|
port := uint16(8080)
|
||||||
|
|
||||||
|
dbURL := os.Getenv("DATABASE_URL")
|
||||||
|
if dbURL == "" {
|
||||||
|
log.Fatal("DATABASE_URL is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
connect, err := pgxpool.New(ctx, dbURL)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer connect.Close()
|
||||||
|
repo := repository.New(connect)
|
||||||
|
j := jwt.New("yomama")
|
||||||
|
go func() {
|
||||||
|
s := server.New(addr, port, repo, j)
|
||||||
|
err = s.Start()
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Printf("Server is running on %s:%d", addr, port)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err = <-errChan:
|
||||||
|
log.Fatalf("service error: %w", err)
|
||||||
|
case sig := <-signalChan:
|
||||||
|
log.Printf("Received signal %s, initiating graceful shutdown", sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user