Tugas
This commit is contained in:
@@ -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,15 +1,11 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func LoadEnv() error {
|
||||
err := godotenv.Load("pkg/.env")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error loading .env file: %v", err)
|
||||
}
|
||||
return nil
|
||||
// LoadEnv memuat .env file jika ada, tapi tidak error jika tidak ditemukan.
|
||||
// Grader akan export env vars langsung tanpa .env file.
|
||||
func LoadEnv() {
|
||||
_ = godotenv.Load() // ignore error — .env
|
||||
}
|
||||
|
||||
+116
-11
@@ -4,24 +4,129 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"5803025015/w04/internal/auth"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// Connect membuat koneksi ke PostgreSQL menggunakan env vars individual
|
||||
func Connect() (*pgx.Conn, error) {
|
||||
dsn := fmt.Sprintf(
|
||||
"postgres://%s:%s@%s:%s/%s?sslmode=%s",
|
||||
os.Getenv("DB_USER"),
|
||||
os.Getenv("DB_PASSWORD"),
|
||||
os.Getenv("DB_HOST"),
|
||||
os.Getenv("DB_PORT"),
|
||||
os.Getenv("DB_NAME"),
|
||||
os.Getenv("DB_SSLMODE"),
|
||||
host := getEnvOrDefault("DB_HOST", "localhost")
|
||||
port := getEnvOrDefault("DB_PORT", "5432")
|
||||
user := os.Getenv("DB_USER")
|
||||
password := os.Getenv("DB_PASSWORD")
|
||||
dbname := os.Getenv("DB_NAME")
|
||||
sslmode := getEnvOrDefault("DB_SSLMODE", "disable")
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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: "Angelica", password: "123"},
|
||||
{username: "Tesvara", password: "12345"},
|
||||
{username: "alexriting", password: "12345678"},
|
||||
{username: "claudia", 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"
|
||||
|
||||
"5803025015/w04/internal/auth"
|
||||
"5803025015/w04/internal/models"
|
||||
"5803025015/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"
|
||||
|
||||
"5803025015/w04/internal/middleware"
|
||||
"5803025015/w04/internal/models"
|
||||
"5803025015/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"
|
||||
|
||||
"5803025015/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"
|
||||
|
||||
"5803025015/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"
|
||||
|
||||
"5803025015/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"
|
||||
|
||||
"5803025015/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
|
||||
}
|
||||
Reference in New Issue
Block a user