Tugas
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
# Server config
|
||||||
|
#
|
||||||
|
PORT=4000
|
||||||
|
ENVIRONMENT=dev # option: dev, stage, prod
|
||||||
|
|
||||||
|
# PostgreSQL Database Config
|
||||||
|
#
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=your_user
|
||||||
|
DB_PASSWORD=your_secure_password
|
||||||
|
DB_NAME=your_database_name
|
||||||
|
DB_SSLMODE=disable # 'disable' or 'require'
|
||||||
|
|
||||||
|
DATABASE_URL=your_database_url
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
+40
-10
@@ -3,18 +3,19 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"5803025024/w04/internal/config"
|
"5803025024/w04/internal/config"
|
||||||
"5803025024/w04/internal/database"
|
"5803025024/w04/internal/database"
|
||||||
|
"5803025024/w04/internal/handler"
|
||||||
|
"5803025024/w04/internal/middleware"
|
||||||
|
"5803025024/w04/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := config.LoadEnv(); err != nil {
|
config.LoadEnv()
|
||||||
fmt.Fprintf(os.Stderr, "Unable to load env: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := database.Connect()
|
conn, err := database.Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -23,13 +24,42 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer conn.Close(context.Background())
|
defer conn.Close(context.Background())
|
||||||
|
|
||||||
var name string
|
err = database.CreateTables(conn)
|
||||||
var weight int64
|
|
||||||
err = conn.QueryRow(context.Background(), "select name, weight from widgets where id=$1", 42).Scan(&name, &weight)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Failed to create tables: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(name, weight)
|
fmt.Println("Database connected and tables created successfully!")
|
||||||
|
|
||||||
|
mu := &sync.Mutex{}
|
||||||
|
userStore := store.NewUserStore(conn, mu)
|
||||||
|
sessionStore := store.NewSessionStore(conn, mu)
|
||||||
|
snippetStore := store.NewSnippetStore(conn, mu)
|
||||||
|
|
||||||
|
authHandler := handler.NewAuthHandler(userStore, sessionStore)
|
||||||
|
snippetHandler := handler.NewSnippetHandler(snippetStore)
|
||||||
|
authMiddleware := middleware.NewAuthMiddleware(sessionStore)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
mux.HandleFunc("POST /register", authHandler.Register)
|
||||||
|
mux.HandleFunc("POST /login", authHandler.Login)
|
||||||
|
mux.Handle("POST /logout", authMiddleware.RequireAuth(http.HandlerFunc(authHandler.Logout)))
|
||||||
|
|
||||||
|
mux.Handle("GET /snippets", authMiddleware.RequireAuth(http.HandlerFunc(snippetHandler.List)))
|
||||||
|
mux.Handle("POST /snippets", authMiddleware.RequireAuth(http.HandlerFunc(snippetHandler.Create)))
|
||||||
|
mux.Handle("GET /snippets/{id}", authMiddleware.RequireAuth(http.HandlerFunc(snippetHandler.Get)))
|
||||||
|
mux.Handle("DELETE /snippets/{id}", authMiddleware.RequireAuth(http.HandlerFunc(snippetHandler.Delete)))
|
||||||
|
|
||||||
|
port := os.Getenv("PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "8080"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Server listening on :%s\n", port)
|
||||||
|
if err := http.ListenAndServe(":"+port, mux); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Server error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,15 @@ module 5803025024/w04
|
|||||||
|
|
||||||
go 1.26.6
|
go 1.26.6
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/jackc/pgx/v5 v5.11.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
golang.org/x/crypto v0.57.0
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.11.0 // indirect
|
golang.org/x/text v0.42.0 // indirect
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
|
||||||
golang.org/x/text v0.29.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,32 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
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 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
|
github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
|
||||||
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
|
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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||||
|
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||||
|
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||||
|
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||||
|
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||||
|
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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.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,25 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestPasswordAndToken(t *testing.T) {
|
||||||
|
pass := "secret123"
|
||||||
|
hash, err := HashPassword(pass)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error hashing: %v", err)
|
||||||
|
}
|
||||||
|
if !CheckPassword(pass, hash) {
|
||||||
|
t.Fatal("expected password check to succeed")
|
||||||
|
}
|
||||||
|
if CheckPassword("wrongpass", hash) {
|
||||||
|
t.Fatal("expected wrong password to fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
tok, err := GenerateToken()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error generating token: %v", err)
|
||||||
|
}
|
||||||
|
if len(tok) != 64 {
|
||||||
|
t.Fatalf("expected 64 hex chars, got %d", len(tok))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckPassword(password, hash string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateToken() (string, error) {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b), nil
|
||||||
|
}
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadEnv() error {
|
// LoadEnv memuat .env file jika ada, tapi tidak error jika tidak ditemukan.
|
||||||
err := godotenv.Load("pkg/.env")
|
// Grader akan export env vars langsung tanpa .env file.
|
||||||
if err != nil {
|
func LoadEnv() {
|
||||||
return fmt.Errorf("Error loading .env file: %v", err)
|
_ = godotenv.Load() // ignore error — .env
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
+114
-10
@@ -4,24 +4,128 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"5803025024/w04/internal/auth"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Connect membuat koneksi ke PostgreSQL menggunakan env vars individual
|
||||||
func Connect() (*pgx.Conn, error) {
|
func Connect() (*pgx.Conn, error) {
|
||||||
dsn := fmt.Sprintf(
|
host := getEnvOrDefault("DB_HOST", "localhost")
|
||||||
"postgres://%s:%s@%s:%s/%s?sslmode=%s",
|
port := getEnvOrDefault("DB_PORT", "5432")
|
||||||
os.Getenv("DB_USER"),
|
user := os.Getenv("DB_USER")
|
||||||
os.Getenv("DB_PASSWORD"),
|
password := os.Getenv("DB_PASSWORD")
|
||||||
os.Getenv("DB_HOST"),
|
dbname := os.Getenv("DB_NAME")
|
||||||
os.Getenv("DB_PORT"),
|
sslmode := getEnvOrDefault("DB_SSLMODE", "disable")
|
||||||
os.Getenv("DB_NAME"),
|
|
||||||
os.Getenv("DB_SSLMODE"),
|
connStr := fmt.Sprintf(
|
||||||
|
"host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
|
||||||
|
host, port, user, password, dbname, sslmode,
|
||||||
)
|
)
|
||||||
|
|
||||||
conn, err := pgx.Connect(context.Background(), dsn)
|
conn, err := pgx.Connect(context.Background(), connStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Unable to connect to database: %v", err)
|
return nil, fmt.Errorf("unable to connect to database: %v", err)
|
||||||
}
|
}
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateTables membuat tabel users, sessions, dan snippets jika belum ada
|
||||||
|
func CreateTables(conn *pgx.Conn) error {
|
||||||
|
queries := []string{
|
||||||
|
`CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS sessions (
|
||||||
|
token TEXT PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
)`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS snippets (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
)`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, q := range queries {
|
||||||
|
_, err := conn.Exec(context.Background(), q)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create table: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otomatis masukkan data sample 2 user & session
|
||||||
|
if err := SeedSampleData(conn); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: failed to seed sample data: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeedSampleData memasukkan data sample 2 user dan session ke database dan mencetak infonya langsung ke konsol VS Code
|
||||||
|
func SeedSampleData(conn *pgx.Conn) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
samples := []struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}{
|
||||||
|
{username: "Adi", password: "123"},
|
||||||
|
{username: "Vz", password: "12345"},
|
||||||
|
{username: "Penipu", password: "12345678"},
|
||||||
|
{username: "Tele", password: "1234567890"},
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("✅ INSERT DATA SAMPLE KE DATABASE (lebih dari 2 USERS):")
|
||||||
|
|
||||||
|
for i, s := range samples {
|
||||||
|
userID := uuid.New().String()
|
||||||
|
hash, err := auth.HashPassword(s.password)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to hash password for %s: %w", s.username, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.QueryRow(ctx,
|
||||||
|
"INSERT INTO users (id, username, password_hash) VALUES ($1, $2, $3) ON CONFLICT (username) DO UPDATE SET password_hash = EXCLUDED.password_hash RETURNING id",
|
||||||
|
userID, s.username, hash,
|
||||||
|
).Scan(&userID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to insert user %s: %w", s.username, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := auth.GenerateToken()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate session token for %s: %w", s.username, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = conn.Exec(ctx,
|
||||||
|
"INSERT INTO sessions (token, user_id, created_at) VALUES ($1, $2, $3)",
|
||||||
|
token, userID, time.Now(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to insert session for %s: %w", s.username, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\n--- [User %d: %s] ---\n", i+1, s.username)
|
||||||
|
fmt.Printf(" User ID : %s\n", userID)
|
||||||
|
fmt.Printf(" Username : %s\n", s.username)
|
||||||
|
fmt.Printf(" Password Plain: %s\n", s.password)
|
||||||
|
fmt.Printf(" Password Hash : %s\n", hash)
|
||||||
|
fmt.Printf(" Session Token : %s\n", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvOrDefault(key, defaultVal string) string {
|
||||||
|
if val := os.Getenv(key); val != "" {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"5803025024/w04/internal/auth"
|
||||||
|
"5803025024/w04/internal/models"
|
||||||
|
"5803025024/w04/internal/store"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthHandler struct {
|
||||||
|
userStore *store.UserStore
|
||||||
|
sessionStore *store.SessionStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthHandler(userStore *store.UserStore, sessionStore *store.SessionStore) *AuthHandler {
|
||||||
|
return &AuthHandler{
|
||||||
|
userStore: userStore,
|
||||||
|
sessionStore: sessionStore,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type authRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req authRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Username = strings.TrimSpace(req.Username)
|
||||||
|
if len(req.Username) < 3 || len(req.Username) > 32 || req.Password == "" {
|
||||||
|
http.Error(w, "Invalid username or password", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := auth.HashPassword(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
Username: req.Username,
|
||||||
|
PasswordHash: hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.userStore.Create(r.Context(), user); err != nil {
|
||||||
|
http.Error(w, "Username already exists or create failed", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_ = json.NewEncoder(w).Encode(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req authRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.userStore.GetByUsername(r.Context(), req.Username)
|
||||||
|
if err != nil || !auth.CheckPassword(req.Password, user.PasswordHash) {
|
||||||
|
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := auth.GenerateToken()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session := &models.Session{
|
||||||
|
Token: token,
|
||||||
|
UserID: user.ID,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.sessionStore.Create(r.Context(), session); err != nil {
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token := authHeader
|
||||||
|
if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
|
||||||
|
token = authHeader[7:]
|
||||||
|
}
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
if token == "" {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.sessionStore.Delete(r.Context(), token); err != nil {
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{"message": "logged out"})
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"5803025024/w04/internal/middleware"
|
||||||
|
"5803025024/w04/internal/models"
|
||||||
|
"5803025024/w04/internal/store"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SnippetHandler struct {
|
||||||
|
snippetStore *store.SnippetStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSnippetHandler(snippetStore *store.SnippetStore) *SnippetHandler {
|
||||||
|
return &SnippetHandler{snippetStore: snippetStore}
|
||||||
|
}
|
||||||
|
|
||||||
|
type snippetRequest struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SnippetHandler) Snippets(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
h.List(w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
h.Create(w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SnippetHandler) SnippetByID(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
h.Get(w, r)
|
||||||
|
case http.MethodDelete:
|
||||||
|
h.Delete(w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SnippetHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := middleware.GetUserID(r.Context())
|
||||||
|
if userID == "" {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req snippetRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Title = strings.TrimSpace(req.Title)
|
||||||
|
req.Content = strings.TrimSpace(req.Content)
|
||||||
|
if req.Title == "" || req.Content == "" {
|
||||||
|
http.Error(w, "Title and content are required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
snippet := &models.Snippet{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
Title: req.Title,
|
||||||
|
Content: req.Content,
|
||||||
|
UserID: userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.snippetStore.Create(r.Context(), snippet); err != nil {
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_ = json.NewEncoder(w).Encode(snippet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SnippetHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
snippets, err := h.snippetStore.GetAll(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(snippets)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SnippetHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := r.PathValue("id")
|
||||||
|
if id == "" {
|
||||||
|
path := strings.TrimPrefix(r.URL.Path, "/snippets/")
|
||||||
|
id = strings.TrimSuffix(path, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
snippet, err := h.snippetStore.GetByID(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
http.Error(w, "Snippet not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(snippet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SnippetHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodDelete {
|
||||||
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := r.PathValue("id")
|
||||||
|
if id == "" {
|
||||||
|
path := strings.TrimPrefix(r.URL.Path, "/snippets/")
|
||||||
|
id = strings.TrimSuffix(path, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
if id == "" {
|
||||||
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.snippetStore.Delete(r.Context(), id); err != nil {
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"5803025024/w04/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const UserIDKey contextKey = "user_id"
|
||||||
|
|
||||||
|
func GetUserID(ctx context.Context) string {
|
||||||
|
if val, ok := ctx.Value(UserIDKey).(string); ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthMiddleware struct {
|
||||||
|
sessionStore *store.SessionStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthMiddleware(sessionStore *store.SessionStore) *AuthMiddleware {
|
||||||
|
return &AuthMiddleware{sessionStore: sessionStore}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AuthMiddleware) RequireAuth(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
authHeader := r.Header.Get("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token := authHeader
|
||||||
|
if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
|
||||||
|
token = authHeader[7:]
|
||||||
|
}
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
if token == "" {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := m.sessionStore.Get(r.Context(), token)
|
||||||
|
if err != nil || session == nil {
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.WithValue(r.Context(), UserIDKey, session.UserID)
|
||||||
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *AuthMiddleware) RequireAuthFunc(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
m.RequireAuth(next).ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequireAuth(sessionStore *store.SessionStore) func(http.Handler) http.Handler {
|
||||||
|
return NewAuthMiddleware(sessionStore).RequireAuth
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User merepresentasikan tabel users di database
|
||||||
|
type User struct {
|
||||||
|
ID string `json:"id"` // UUID, server-generated
|
||||||
|
Username string `json:"username"` // unique, case-sensitive, 3-32 chars
|
||||||
|
PasswordHash string `json:"-"` // never returned in any response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session merepresentasikan tabel sessions di database
|
||||||
|
type Session struct {
|
||||||
|
Token string `json:"token"` // server-generated, unique bearer token
|
||||||
|
UserID string `json:"user_id"` // foreign key ke users.id
|
||||||
|
CreatedAt time.Time `json:"created_at"` // informational only
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snippet merepresentasikan tabel snippets di database
|
||||||
|
// UserID disimpan di DB tapi tidak di-serialize ke JSON response
|
||||||
|
type Snippet struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
UserID string `json:"-"` // internal only, not in JSON response
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"5803025024/w04/internal/models"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionStore struct {
|
||||||
|
conn *pgx.Conn
|
||||||
|
mu *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSessionStore(conn *pgx.Conn, optMu ...*sync.Mutex) *SessionStore {
|
||||||
|
var mu *sync.Mutex
|
||||||
|
if len(optMu) > 0 && optMu[0] != nil {
|
||||||
|
mu = optMu[0]
|
||||||
|
} else {
|
||||||
|
mu = &sync.Mutex{}
|
||||||
|
}
|
||||||
|
return &SessionStore{conn: conn, mu: mu}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionStore) Create(ctx context.Context, session *models.Session) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if session.CreatedAt.IsZero() {
|
||||||
|
session.CreatedAt = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `INSERT INTO sessions (token, user_id, created_at) VALUES ($1, $2, $3)`
|
||||||
|
_, err := s.conn.Exec(ctx, query, session.Token, session.UserID, session.CreatedAt)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionStore) Get(ctx context.Context, token string) (*models.Session, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
var sess models.Session
|
||||||
|
query := `SELECT token, user_id, created_at FROM sessions WHERE token = $1`
|
||||||
|
err := s.conn.QueryRow(ctx, query, token).Scan(&sess.Token, &sess.UserID, &sess.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sess, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SessionStore) Delete(ctx context.Context, token string) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
query := `DELETE FROM sessions WHERE token = $1`
|
||||||
|
_, err := s.conn.Exec(ctx, query, token)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"5803025024/w04/internal/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SnippetStore struct {
|
||||||
|
conn *pgx.Conn
|
||||||
|
mu *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSnippetStore(conn *pgx.Conn, optMu ...*sync.Mutex) *SnippetStore {
|
||||||
|
var mu *sync.Mutex
|
||||||
|
if len(optMu) > 0 && optMu[0] != nil {
|
||||||
|
mu = optMu[0]
|
||||||
|
} else {
|
||||||
|
mu = &sync.Mutex{}
|
||||||
|
}
|
||||||
|
return &SnippetStore{conn: conn, mu: mu}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SnippetStore) Create(ctx context.Context, snippet *models.Snippet) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if snippet.ID == "" {
|
||||||
|
snippet.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `INSERT INTO snippets (id, title, content, user_id) VALUES ($1, $2, $3, $4)`
|
||||||
|
_, err := s.conn.Exec(ctx, query, snippet.ID, snippet.Title, snippet.Content, snippet.UserID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SnippetStore) GetByID(ctx context.Context, id string) (*models.Snippet, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
var sn models.Snippet
|
||||||
|
query := `SELECT id, title, content, user_id FROM snippets WHERE id = $1`
|
||||||
|
err := s.conn.QueryRow(ctx, query, id).Scan(&sn.ID, &sn.Title, &sn.Content, &sn.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SnippetStore) GetAll(ctx context.Context) ([]*models.Snippet, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
query := `SELECT id, title, content, user_id FROM snippets`
|
||||||
|
rows, err := s.conn.Query(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
snippets := make([]*models.Snippet, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var sn models.Snippet
|
||||||
|
if err := rows.Scan(&sn.ID, &sn.Title, &sn.Content, &sn.UserID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
snippets = append(snippets, &sn)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return snippets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SnippetStore) Delete(ctx context.Context, id string) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
query := `DELETE FROM snippets WHERE id = $1`
|
||||||
|
_, err := s.conn.Exec(ctx, query, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"5803025024/w04/internal/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserStore struct {
|
||||||
|
conn *pgx.Conn
|
||||||
|
mu *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserStore(conn *pgx.Conn, optMu ...*sync.Mutex) *UserStore {
|
||||||
|
var mu *sync.Mutex
|
||||||
|
if len(optMu) > 0 && optMu[0] != nil {
|
||||||
|
mu = optMu[0]
|
||||||
|
} else {
|
||||||
|
mu = &sync.Mutex{}
|
||||||
|
}
|
||||||
|
return &UserStore{conn: conn, mu: mu}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserStore) Create(ctx context.Context, user *models.User) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if user.ID == "" {
|
||||||
|
user.ID = uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `INSERT INTO users (id, username, password_hash) VALUES ($1, $2, $3)`
|
||||||
|
_, err := s.conn.Exec(ctx, query, user.ID, user.Username, user.PasswordHash)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserStore) GetByUsername(ctx context.Context, username string) (*models.User, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
var u models.User
|
||||||
|
query := `SELECT id, username, password_hash FROM users WHERE username = $1`
|
||||||
|
err := s.conn.QueryRow(ctx, query, username).Scan(&u.ID, &u.Username, &u.PasswordHash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserStore) GetByID(ctx context.Context, id string) (*models.User, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
var u models.User
|
||||||
|
query := `SELECT id, username, password_hash FROM users WHERE id = $1`
|
||||||
|
err := s.conn.QueryRow(ctx, query, id).Scan(&u.ID, &u.Username, &u.PasswordHash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Server Configuration
|
|
||||||
#
|
|
||||||
PORT=4000
|
|
||||||
ENVIROMENT=dev # Options: dev, stage, prod
|
|
||||||
|
|
||||||
# PostgreSQL Database Configuration
|
|
||||||
#
|
|
||||||
DB_HOST=202.46.28.160
|
|
||||||
DB_PORT=2003
|
|
||||||
DB_USER=5803025024
|
|
||||||
DB_PASSWORD=pw5803025024
|
|
||||||
DB_NAME=w04_5803025024
|
|
||||||
DB_SSLMODE=disable # 'disable' or 'require'
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Server Configuration
|
|
||||||
#
|
|
||||||
PORT=4000
|
|
||||||
ENVIROMENT=dev # Options: dev, stage, prod
|
|
||||||
|
|
||||||
# PostgreSQL Database Configuration
|
|
||||||
#
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_USER=postgres
|
|
||||||
DB_PASSWORD=5803025024
|
|
||||||
DB_NAME=snippetboox
|
|
||||||
DB_SSLMODE=disable # 'disable' or 'require'
|
|
||||||
Reference in New Issue
Block a user