Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af9c23c521 | ||
|
|
715c122eb7 | ||
|
|
09804829bd | ||
|
|
f83a8b0697 |
@@ -1,3 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
.dockerenv
|
.dockerenv
|
||||||
node_modules
|
node_modules
|
||||||
|
tmp
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
env "local" {
|
||||||
|
src = "file://migrations/schema.sql"
|
||||||
|
url = "postgres://postgres:postgres@localhost:5433/go_weather?sslmode=disable"
|
||||||
|
dev = "docker://postgres/16/alpine/dev" # for schema validation
|
||||||
|
}
|
||||||
|
|
||||||
|
env "production" {
|
||||||
|
src = "file://migrations/schema.sql"
|
||||||
|
url = "postgres://${env("DB_USER")}:${env("DB_PASSWORD")}@${env("DB_HOST")}:${env("DB_PORT")}/${env("DB_NAME")}?sslmode=require"
|
||||||
|
dev = "docker://postgres/16/alpine/dev"
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ type Config struct {
|
|||||||
DBUser string
|
DBUser string
|
||||||
DBPassword string
|
DBPassword string
|
||||||
DBName string
|
DBName string
|
||||||
|
JWTSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetEnv(key string) string {
|
func GetEnv(key string) string {
|
||||||
@@ -32,5 +33,6 @@ func Load() *Config {
|
|||||||
DBUser: GetEnv("DB_USER"),
|
DBUser: GetEnv("DB_USER"),
|
||||||
DBPassword: GetEnv("DB_PASSWORD"),
|
DBPassword: GetEnv("DB_PASSWORD"),
|
||||||
DBName: GetEnv("DB_NAME"),
|
DBName: GetEnv("DB_NAME"),
|
||||||
|
JWTSecret: GetEnv("JWT_SECRET"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ThemeProvider } from "@/components/theme-provider"
|
|||||||
import LatestTemperature from "@/components/latest-temperature"
|
import LatestTemperature from "@/components/latest-temperature"
|
||||||
import TemperatureHistory from "@/components/temperature-history"
|
import TemperatureHistory from "@/components/temperature-history"
|
||||||
import TemperaturePlot from "@/components/temperature-plot"
|
import TemperaturePlot from "@/components/temperature-plot"
|
||||||
|
import RegisterForm from './components/register-form'
|
||||||
// import { Button } from "@/components/ui/button"
|
// import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
|
||||||
@@ -12,6 +13,9 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||||
<main className="min-h-screen bg-background p-6 w-full">
|
<main className="min-h-screen bg-background p-6 w-full">
|
||||||
|
<div>
|
||||||
|
<RegisterForm />
|
||||||
|
</div>
|
||||||
<div className="w-full mx-auto space-y-2">
|
<div className="w-full mx-auto space-y-2">
|
||||||
<TemperaturePlot />
|
<TemperaturePlot />
|
||||||
<div className="grid grid-cols-4 gap-6">
|
<div className="grid grid-cols-4 gap-6">
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
export default function RegisterForm() {
|
||||||
|
const [email, setEmail] = useState("")
|
||||||
|
const [password, setPassword] = useState("")
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("")
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [success, setSuccess] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
|
setError(null)
|
||||||
|
setSuccess(false)
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError("Passwords do not match")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
setError("Passwords must be at least 8 characters")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/register", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ email, password }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const msg = await res.text()
|
||||||
|
setError(msg || "Registration failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuccess(true)
|
||||||
|
setEmail("")
|
||||||
|
setPassword("")
|
||||||
|
setConfirmPassword("")
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
setError("Network error")
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="p-4 m-6 flex flex-col border rounded-lg bg-card">
|
||||||
|
<h2>Register</h2>
|
||||||
|
{error && <p>{error}</p>}
|
||||||
|
{success && <p>Account created</p>}
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
className="p-2 border rounded bg-background"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="p-2 border rounded bg-background"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="p-2 border rounded bg-background"
|
||||||
|
/>
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
{loading ? "Creating ..." : "Register"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -72,24 +72,6 @@ h1 {
|
|||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -3,3 +3,8 @@ module go-weather
|
|||||||
go 1.25.6
|
go 1.25.6
|
||||||
|
|
||||||
require github.com/lib/pq v1.11.1
|
require github.com/lib/pq v1.11.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||||
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI=
|
github.com/lib/pq v1.11.1 h1:wuChtj2hfsGmmx3nf1m7xC2XpK6OtelS2shMY+bGMtI=
|
||||||
github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
github.com/lib/pq v1.11.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"go-weather/models"
|
||||||
|
"go-weather/services"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserHandler struct {
|
||||||
|
userService *services.UserService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserHandler(us *services.UserService) *UserHandler {
|
||||||
|
return &UserHandler{userService: us}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req models.RegisterRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Email = strings.TrimSpace(strings.ToLower(req.Email))
|
||||||
|
if req.Email == "" || req.Password == "" {
|
||||||
|
http.Error(w, "email and password are required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Password) < 8 {
|
||||||
|
http.Error(w, "password must be at least 8 characters", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := h.userService.EmailExists(req.Email)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
http.Error(w, "email already registered", http.StatusConflict)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.userService.CreateUser(req.Email, req.Password)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to create user", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(user)
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
_ "github.com/lib/pq"
|
|
||||||
"go-weather/config"
|
"go-weather/config"
|
||||||
"go-weather/handlers"
|
"go-weather/handlers"
|
||||||
"go-weather/services"
|
"go-weather/services"
|
||||||
@@ -12,6 +11,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
func startPoller(db *sql.DB, ws *services.WeatherService, defaultCity string, interval time.Duration) {
|
func startPoller(db *sql.DB, ws *services.WeatherService, defaultCity string, interval time.Duration) {
|
||||||
@@ -58,18 +59,15 @@ func main() {
|
|||||||
log.Fatalf("failed to connect to db: %v", err)
|
log.Fatalf("failed to connect to db: %v", err)
|
||||||
}
|
}
|
||||||
defer database.Close()
|
defer database.Close()
|
||||||
databaseService := services.NewDatabaseService(database)
|
|
||||||
|
|
||||||
// Setup weather_readings table
|
|
||||||
if err := databaseService.CreateTable(); err != nil {
|
|
||||||
log.Fatalf("failed to create table: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create services
|
// Create services
|
||||||
|
databaseService := services.NewDatabaseService(database)
|
||||||
weatherService := services.NewWeatherService(config.APIKey)
|
weatherService := services.NewWeatherService(config.APIKey)
|
||||||
|
userService := services.NewUserService(database)
|
||||||
|
|
||||||
// Create route handlers
|
// Create route handlers
|
||||||
weatherHandler := handlers.NewWeatherHandler(weatherService, databaseService, config.DefaultCity)
|
weatherHandler := handlers.NewWeatherHandler(weatherService, databaseService, config.DefaultCity)
|
||||||
|
userHandler := handlers.NewUserHandler(userService)
|
||||||
|
|
||||||
// Start background poller
|
// Start background poller
|
||||||
go startPoller(database, weatherService, config.DefaultCity, 10*time.Minute)
|
go startPoller(database, weatherService, config.DefaultCity, 10*time.Minute)
|
||||||
@@ -80,6 +78,7 @@ func main() {
|
|||||||
http.HandleFunc("/health", handlers.Health)
|
http.HandleFunc("/health", handlers.Health)
|
||||||
http.HandleFunc("/latest-temperature", handlers.LatestTemperature)
|
http.HandleFunc("/latest-temperature", handlers.LatestTemperature)
|
||||||
http.HandleFunc("/test", handlers.Test)
|
http.HandleFunc("/test", handlers.Test)
|
||||||
|
http.HandleFunc("/register", userHandler.Register)
|
||||||
// Start server
|
// Start server
|
||||||
go func() {
|
go func() {
|
||||||
fmt.Println("server listening on :8080")
|
fmt.Println("server listening on :8080")
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS weather_readings (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
city TEXT NOT NULL,
|
||||||
|
temp DOUBLE PRECISION NOT NULL,
|
||||||
|
feels_like DOUBLE PRECISION NOT NULL,
|
||||||
|
humidity INTEGER NOT NULL,
|
||||||
|
conditions TEXT NOT NULL,
|
||||||
|
fetched_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
PasswordHash string `json:"-"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginResponse struct {
|
||||||
|
User User `json:"user"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: postgres-dev
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
POSTGRES_DB: go_weather
|
||||||
|
ports:
|
||||||
|
- "5433:5432"
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
cloudbeaver:
|
||||||
|
image: dbeaver/cloudbeaver:latest
|
||||||
|
container_name: cloudbeaver-dev
|
||||||
|
ports:
|
||||||
|
- "8978:8978"
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Claims struct {
|
||||||
|
UserID int `json:"user_id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthService struct {
|
||||||
|
secretKey []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthService(secret string) *AuthService {
|
||||||
|
return &AuthService{secretKey: []byte(secret)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) GenerateToken(userID int, email string) (string, error) {
|
||||||
|
claims := Claims{
|
||||||
|
UserID: userID,
|
||||||
|
Email: email,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
|
||||||
|
return token.SignedString(s.secretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) ValidateToken(tokenString string) (*Claims, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, &Claims, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, errors.New("unexpected signing method")
|
||||||
|
}
|
||||||
|
return s.secretKey, nil
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("invalid token")
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
"go-weather/models"
|
"go-weather/models"
|
||||||
"log"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,26 +31,6 @@ func NewDatabaseService(db *sql.DB) *DatabaseService {
|
|||||||
return &DatabaseService{db: db}
|
return &DatabaseService{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DatabaseService) CreateTable() error {
|
|
||||||
_, err := s.db.Exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS weather_readings (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
city TEXT NOT NULL,
|
|
||||||
temp DOUBLE PRECISION NOT NULL,
|
|
||||||
feels_like DOUBLE PRECISION NOT NULL,
|
|
||||||
humidity INTEGER NOT NULL,
|
|
||||||
conditions TEXT NOT NULL,
|
|
||||||
fetched_at TIMESTAMP WITH TIME ZONE NOT NULL
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("failed to create table: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Println("weather_readings table ready")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *DatabaseService) SaveReading(weather models.WeatherResponse) (int, error) {
|
func (s *DatabaseService) SaveReading(weather models.WeatherResponse) (int, error) {
|
||||||
conditions := ""
|
conditions := ""
|
||||||
if len(weather.Weather) > 0 {
|
if len(weather.Weather) > 0 {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"go-weather/models"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserService struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserService(db *sql.DB) *UserService {
|
||||||
|
return &UserService{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) CreateUser(email string, password string) (*models.User, error) {
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
err = s.db.QueryRow(
|
||||||
|
`INSERT INTO users (email, password_hash, created_at)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id, email, created_at`,
|
||||||
|
email, string(hashedPassword), time.Now(),
|
||||||
|
).Scan(&user.ID, &user.Email, &user.CreatedAt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) GetUserByEmail(email string) (*models.User, error) {
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
err := s.db.QueryRow(
|
||||||
|
`SELECT id, email, password_hash, created_at
|
||||||
|
FROM users
|
||||||
|
WHERE email = $1`, email,
|
||||||
|
).Scan(&user.ID, &user.Email, &user.PasswordHash, &user.CreatedAt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) EmailExists(email string) (bool, error) {
|
||||||
|
var exists bool
|
||||||
|
err := s.db.QueryRow(`SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)`, email).Scan(&exists)
|
||||||
|
return exists, err
|
||||||
|
}
|
||||||
|
func (s *UserService) ValidatePassword(user *models.User, password string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user