Compare commits
10 Commits
5618aeb439
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e5c409621 | |||
| 66be0a2b40 | |||
| bad1d140ee | |||
| 704393e0e4 | |||
| 1bad21ae45 | |||
| 5fe09f0a2f | |||
| 6661fdf89b | |||
| 647edb6d25 | |||
| bd6583831c | |||
| de5f090a9c |
9
go.mod
9
go.mod
@@ -3,7 +3,10 @@ module github.com/rdarius/go-http-server
|
||||
go 1.23.6
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lib/pq v1.10.9
|
||||
golang.org/x/crypto v0.36.0
|
||||
)
|
||||
|
||||
require github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1,6 +1,10 @@
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
|
||||
107
internal/auth/auth.go
Normal file
107
internal/auth/auth.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TokenType string
|
||||
|
||||
const (
|
||||
// TokenTypeAccess -
|
||||
TokenTypeAccess TokenType = "chirpy-access"
|
||||
)
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
pass, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(pass), err
|
||||
}
|
||||
|
||||
func CheckPasswordHash(password, hash string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
}
|
||||
|
||||
// MakeJWT -
|
||||
func MakeJWT(
|
||||
userID uuid.UUID,
|
||||
tokenSecret string,
|
||||
expiresIn time.Duration,
|
||||
) (string, error) {
|
||||
signingKey := []byte(tokenSecret)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
|
||||
Issuer: string(TokenTypeAccess),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now().UTC()),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().UTC().Add(expiresIn)),
|
||||
Subject: userID.String(),
|
||||
})
|
||||
return token.SignedString(signingKey)
|
||||
}
|
||||
|
||||
// ValidateJWT -
|
||||
func ValidateJWT(tokenString, tokenSecret string) (uuid.UUID, error) {
|
||||
claimsStruct := jwt.RegisteredClaims{}
|
||||
token, err := jwt.ParseWithClaims(
|
||||
tokenString,
|
||||
&claimsStruct,
|
||||
func(token *jwt.Token) (interface{}, error) { return []byte(tokenSecret), nil },
|
||||
)
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
userIDString, err := token.Claims.GetSubject()
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
issuer, err := token.Claims.GetIssuer()
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
if issuer != string(TokenTypeAccess) {
|
||||
return uuid.Nil, errors.New("invalid issuer")
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(userIDString)
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("invalid user ID: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func GetBearerToken(headers http.Header) (string, error) {
|
||||
auth := headers.Get("Authorization")
|
||||
auth = strings.TrimPrefix(auth, "Bearer ")
|
||||
if auth == "" {
|
||||
return "", errors.New("missing Authorization header")
|
||||
}
|
||||
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
func MakeRefreshToken() (string, error) {
|
||||
key := make([]byte, 32)
|
||||
_, err := rand.Read(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token := hex.EncodeToString(key)
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func GetAPIKey(headers http.Header) (string, error) {
|
||||
auth := headers.Get("Authorization")
|
||||
auth = strings.TrimPrefix(auth, "ApiKey ")
|
||||
if auth == "" {
|
||||
return "", errors.New("missing Authorization header")
|
||||
}
|
||||
return auth, nil
|
||||
}
|
||||
110
internal/auth/auth_test.go
Normal file
110
internal/auth/auth_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCheckPasswordHash(t *testing.T) {
|
||||
// First, we need to create some hashed passwords for testing
|
||||
password1 := "correctPassword123!"
|
||||
password2 := "anotherPassword456!"
|
||||
hash1, _ := HashPassword(password1)
|
||||
hash2, _ := HashPassword(password2)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
hash string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Correct password",
|
||||
password: password1,
|
||||
hash: hash1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Incorrect password",
|
||||
password: "wrongPassword",
|
||||
hash: hash1,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Password doesn't match different hash",
|
||||
password: password1,
|
||||
hash: hash2,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Empty password",
|
||||
password: "",
|
||||
hash: hash1,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid hash",
|
||||
password: password1,
|
||||
hash: "invalidhash",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := CheckPasswordHash(tt.password, tt.hash)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("CheckPasswordHash() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateJWT(t *testing.T) {
|
||||
userID := uuid.New()
|
||||
validToken, _ := MakeJWT(userID, "secret", time.Hour)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tokenString string
|
||||
tokenSecret string
|
||||
wantUserID uuid.UUID
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid token",
|
||||
tokenString: validToken,
|
||||
tokenSecret: "secret",
|
||||
wantUserID: userID,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid token",
|
||||
tokenString: "invalid.token.string",
|
||||
tokenSecret: "secret",
|
||||
wantUserID: uuid.Nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Wrong secret",
|
||||
tokenString: validToken,
|
||||
tokenSecret: "wrong_secret",
|
||||
wantUserID: uuid.Nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotUserID, err := ValidateJWT(tt.tokenString, tt.tokenSecret)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateJWT() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotUserID != tt.wantUserID {
|
||||
t.Errorf("ValidateJWT() gotUserID = %v, want %v", gotUserID, tt.wantUserID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
32
internal/dataMaps/maps.go
Normal file
32
internal/dataMaps/maps.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package dataMaps
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Chirp struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Body string `json:"body"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Email string `json:"email"`
|
||||
IsChirpyRed bool `json:"is_chirpy_red"`
|
||||
}
|
||||
|
||||
type UserWithToken struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Email string `json:"email"`
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
IsChirpyRed bool `json:"is_chirpy_red"`
|
||||
}
|
||||
36
internal/database/CreateChirp.sql.go
Normal file
36
internal/database/CreateChirp.sql.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: CreateChirp.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const createChirp = `-- name: CreateChirp :one
|
||||
INSERT INTO chirps (id, created_at, updated_at, body, user_id)
|
||||
VALUES (gen_random_uuid(), NOW(), NOW(), $1, $2)
|
||||
RETURNING id, created_at, updated_at, body, user_id
|
||||
`
|
||||
|
||||
type CreateChirpParams struct {
|
||||
Body string
|
||||
UserID uuid.UUID
|
||||
}
|
||||
|
||||
func (q *Queries) CreateChirp(ctx context.Context, arg CreateChirpParams) (Chirp, error) {
|
||||
row := q.db.QueryRowContext(ctx, createChirp, arg.Body, arg.UserID)
|
||||
var i Chirp
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Body,
|
||||
&i.UserID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
39
internal/database/CreateRefreshToken.sql.go
Normal file
39
internal/database/CreateRefreshToken.sql.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: CreateRefreshToken.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const createRefreshToken = `-- name: CreateRefreshToken :one
|
||||
INSERT INTO refresh_tokens (token, created_at, updated_at, user_id, expires_at, revoked_at)
|
||||
VALUES ($1, NOW(), NOW(), $2, $3, null)
|
||||
RETURNING token, created_at, updated_at, user_id, expires_at, revoked_at
|
||||
`
|
||||
|
||||
type CreateRefreshTokenParams struct {
|
||||
Token string
|
||||
UserID uuid.UUID
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error) {
|
||||
row := q.db.QueryRowContext(ctx, createRefreshToken, arg.Token, arg.UserID, arg.ExpiresAt)
|
||||
var i RefreshToken
|
||||
err := row.Scan(
|
||||
&i.Token,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.UserID,
|
||||
&i.ExpiresAt,
|
||||
&i.RevokedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -10,19 +10,26 @@ import (
|
||||
)
|
||||
|
||||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (id, created_at, updated_at, email)
|
||||
VALUES (gen_random_uuid(), NOW(), NOW(), $1)
|
||||
RETURNING id, created_at, updated_at, email
|
||||
INSERT INTO users (id, created_at, updated_at, email, hashed_password)
|
||||
VALUES (gen_random_uuid(), NOW(), NOW(), $1, $2)
|
||||
RETURNING id, created_at, updated_at, email, hashed_password, is_chirpy_red
|
||||
`
|
||||
|
||||
func (q *Queries) CreateUser(ctx context.Context, email string) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, createUser, email)
|
||||
type CreateUserParams struct {
|
||||
Email string
|
||||
HashedPassword string
|
||||
}
|
||||
|
||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, createUser, arg.Email, arg.HashedPassword)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Email,
|
||||
&i.HashedPassword,
|
||||
&i.IsChirpyRed,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
21
internal/database/DeleteChirpByID.sql.go
Normal file
21
internal/database/DeleteChirpByID.sql.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: DeleteChirpByID.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const deleteChirpByID = `-- name: DeleteChirpByID :exec
|
||||
DELETE FROM chirps WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteChirpByID(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteChirpByID, id)
|
||||
return err
|
||||
}
|
||||
43
internal/database/GetAllChirps.sql.go
Normal file
43
internal/database/GetAllChirps.sql.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: GetAllChirps.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const getAllChirps = `-- name: GetAllChirps :many
|
||||
SELECT id, created_at, updated_at, body, user_id FROM chirps ORDER BY created_at
|
||||
`
|
||||
|
||||
func (q *Queries) GetAllChirps(ctx context.Context) ([]Chirp, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAllChirps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Chirp
|
||||
for rows.Next() {
|
||||
var i Chirp
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Body,
|
||||
&i.UserID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
29
internal/database/GetChirpByID.sql.go
Normal file
29
internal/database/GetChirpByID.sql.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: GetChirpByID.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const getChirpByID = `-- name: GetChirpByID :one
|
||||
SELECT id, created_at, updated_at, body, user_id FROM chirps WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetChirpByID(ctx context.Context, id uuid.UUID) (Chirp, error) {
|
||||
row := q.db.QueryRowContext(ctx, getChirpByID, id)
|
||||
var i Chirp
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Body,
|
||||
&i.UserID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
45
internal/database/GetChirpsByUserID.sql.go
Normal file
45
internal/database/GetChirpsByUserID.sql.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: GetChirpsByUserID.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const getChirpsByUserID = `-- name: GetChirpsByUserID :many
|
||||
SELECT id, created_at, updated_at, body, user_id FROM chirps WHERE user_id = $1 ORDER BY created_at
|
||||
`
|
||||
|
||||
func (q *Queries) GetChirpsByUserID(ctx context.Context, userID uuid.UUID) ([]Chirp, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getChirpsByUserID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Chirp
|
||||
for rows.Next() {
|
||||
var i Chirp
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Body,
|
||||
&i.UserID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
28
internal/database/GetRefreshTokenByToken.sql.go
Normal file
28
internal/database/GetRefreshTokenByToken.sql.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: GetRefreshTokenByToken.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const getRefreshTokenByToken = `-- name: GetRefreshTokenByToken :one
|
||||
SELECT token, created_at, updated_at, user_id, expires_at, revoked_at FROM refresh_tokens WHERE token = $1 AND revoked_at is null
|
||||
`
|
||||
|
||||
func (q *Queries) GetRefreshTokenByToken(ctx context.Context, token string) (RefreshToken, error) {
|
||||
row := q.db.QueryRowContext(ctx, getRefreshTokenByToken, token)
|
||||
var i RefreshToken
|
||||
err := row.Scan(
|
||||
&i.Token,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.UserID,
|
||||
&i.ExpiresAt,
|
||||
&i.RevokedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
28
internal/database/GetUserByEmail.sql.go
Normal file
28
internal/database/GetUserByEmail.sql.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: GetUserByEmail.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||
SELECT id, created_at, updated_at, email, hashed_password, is_chirpy_red FROM users WHERE email = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserByEmail, email)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Email,
|
||||
&i.HashedPassword,
|
||||
&i.IsChirpyRed,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
30
internal/database/GetUserByID.sql.go
Normal file
30
internal/database/GetUserByID.sql.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: GetUserByID.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, created_at, updated_at, email, hashed_password, is_chirpy_red FROM users WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserByID, id)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Email,
|
||||
&i.HashedPassword,
|
||||
&i.IsChirpyRed,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
19
internal/database/RevokeRefreshTokenByToken.sql.go
Normal file
19
internal/database/RevokeRefreshTokenByToken.sql.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: RevokeRefreshTokenByToken.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const revokeRefreshTokenByToken = `-- name: RevokeRefreshTokenByToken :exec
|
||||
UPDATE refresh_tokens SET revoked_at = CURRENT_TIMESTAMP WHERE token = $1
|
||||
`
|
||||
|
||||
func (q *Queries) RevokeRefreshTokenByToken(ctx context.Context, token string) error {
|
||||
_, err := q.db.ExecContext(ctx, revokeRefreshTokenByToken, token)
|
||||
return err
|
||||
}
|
||||
37
internal/database/UpdateUserEmailAndPassword.sql.go
Normal file
37
internal/database/UpdateUserEmailAndPassword.sql.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: UpdateUserEmailAndPassword.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const updateUserEmailAndPassword = `-- name: UpdateUserEmailAndPassword :one
|
||||
UPDATE users SET email = $1, hashed_password = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $3
|
||||
RETURNING id, created_at, updated_at, email, hashed_password, is_chirpy_red
|
||||
`
|
||||
|
||||
type UpdateUserEmailAndPasswordParams struct {
|
||||
Email string
|
||||
HashedPassword string
|
||||
ID uuid.UUID
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUserEmailAndPassword(ctx context.Context, arg UpdateUserEmailAndPasswordParams) (User, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateUserEmailAndPassword, arg.Email, arg.HashedPassword, arg.ID)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Email,
|
||||
&i.HashedPassword,
|
||||
&i.IsChirpyRed,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
22
internal/database/UpgradeUserToChirpyRed.sql.go
Normal file
22
internal/database/UpgradeUserToChirpyRed.sql.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: UpgradeUserToChirpyRed.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const upgradeUserToChirpyRed = `-- name: UpgradeUserToChirpyRed :exec
|
||||
UPDATE users SET is_chirpy_red = true WHERE id = $1
|
||||
RETURNING id, created_at, updated_at, email, hashed_password, is_chirpy_red
|
||||
`
|
||||
|
||||
func (q *Queries) UpgradeUserToChirpyRed(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.ExecContext(ctx, upgradeUserToChirpyRed, id)
|
||||
return err
|
||||
}
|
||||
@@ -5,14 +5,34 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Chirp struct {
|
||||
ID uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Body string
|
||||
UserID uuid.UUID
|
||||
}
|
||||
|
||||
type RefreshToken struct {
|
||||
Token string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
UserID uuid.UUID
|
||||
ExpiresAt time.Time
|
||||
RevokedAt sql.NullTime
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uuid.UUID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Email string
|
||||
HashedPassword string
|
||||
IsChirpyRed bool
|
||||
}
|
||||
|
||||
518
main.go
518
main.go
@@ -5,21 +5,28 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/joho/godotenv"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/rdarius/go-http-server/internal/auth"
|
||||
"github.com/rdarius/go-http-server/internal/dataMaps"
|
||||
"github.com/rdarius/go-http-server/internal/database"
|
||||
"github.com/rdarius/go-http-server/internal/httpResponse"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type apiConfig struct {
|
||||
fileserverHits atomic.Int32
|
||||
db database.Queries
|
||||
platform string
|
||||
secret string
|
||||
polkaKey string
|
||||
}
|
||||
|
||||
func (cfg *apiConfig) middlewareMetricsInc(next http.Handler) http.Handler {
|
||||
@@ -74,10 +81,382 @@ func fileServerHandler() http.Handler {
|
||||
return http.StripPrefix("/app", fileServer)
|
||||
}
|
||||
|
||||
func postUsersHandler(cfg *apiConfig) http.HandlerFunc {
|
||||
func getAllChirpsHandler(cfg *apiConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
q := r.URL.Query().Get("author_id")
|
||||
sort := r.URL.Query().Get("sort")
|
||||
|
||||
fmt.Println(sort)
|
||||
|
||||
var chirps []database.Chirp
|
||||
var err error
|
||||
var id uuid.UUID
|
||||
|
||||
if len(q) == 0 {
|
||||
chirps, err = cfg.db.GetAllChirps(context.Background())
|
||||
if err != nil {
|
||||
httpResponse.SomethingWentWrong(w)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
id, err = uuid.Parse(q)
|
||||
if err != nil {
|
||||
httpResponse.SomethingWentWrong(w)
|
||||
return
|
||||
}
|
||||
chirps, err = cfg.db.GetChirpsByUserID(context.Background(), id)
|
||||
if err != nil {
|
||||
httpResponse.SomethingWentWrong(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data := make([]dataMaps.Chirp, 0)
|
||||
for _, chirp := range chirps {
|
||||
data = append(data, dataMaps.Chirp{
|
||||
ID: chirp.ID,
|
||||
CreatedAt: chirp.CreatedAt,
|
||||
UpdatedAt: chirp.UpdatedAt,
|
||||
Body: chirp.Body,
|
||||
UserID: chirp.UserID,
|
||||
})
|
||||
}
|
||||
|
||||
if sort == "desc" {
|
||||
slices.SortFunc(data, func(a, b dataMaps.Chirp) int {
|
||||
if b.CreatedAt.After(a.CreatedAt) {
|
||||
return 1
|
||||
}
|
||||
if b.CreatedAt.Before(a.CreatedAt) {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
httpResponse.SomethingWentWrong(w)
|
||||
}
|
||||
httpResponse.PlainTextHandler(w, http.StatusOK, string(jsonData))
|
||||
}
|
||||
}
|
||||
|
||||
func getChirpByIDHandler(cfg *apiConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
chirpID, err := uuid.Parse(r.PathValue("chirpID"))
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusBadRequest, `{"error": "InvalidChirpID"}`)
|
||||
return
|
||||
}
|
||||
|
||||
chirp, err := cfg.db.GetChirpByID(context.Background(), chirpID)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusNotFound, `{"error": "ChirpNotFound"}`)
|
||||
return
|
||||
}
|
||||
|
||||
data := dataMaps.Chirp{
|
||||
ID: chirp.ID,
|
||||
CreatedAt: chirp.CreatedAt,
|
||||
UpdatedAt: chirp.UpdatedAt,
|
||||
Body: chirp.Body,
|
||||
UserID: chirp.UserID,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
httpResponse.SomethingWentWrong(w)
|
||||
return
|
||||
}
|
||||
httpResponse.PlainTextHandler(w, http.StatusOK, string(jsonData))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteChirpByIDHandler(cfg *apiConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
token, err := auth.GetBearerToken(r.Header)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := auth.ValidateJWT(token, cfg.secret)
|
||||
|
||||
_, err = cfg.db.GetUserByID(context.Background(), userId)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
chirpID, err := uuid.Parse(r.PathValue("chirpID"))
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusBadRequest, `{"error": "InvalidChirpID"}`)
|
||||
return
|
||||
}
|
||||
|
||||
chirp, err := cfg.db.GetChirpByID(context.Background(), chirpID)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusNotFound, `{"error": "ChirpNotFound"}`)
|
||||
return
|
||||
}
|
||||
|
||||
if chirp.UserID != userId {
|
||||
httpResponse.JSONHandler(w, http.StatusForbidden, `{"error": "Forbidden"}`)
|
||||
return
|
||||
}
|
||||
|
||||
err = cfg.db.DeleteChirpByID(context.Background(), chirp.ID)
|
||||
if err != nil {
|
||||
httpResponse.SomethingWentWrong(w)
|
||||
return
|
||||
}
|
||||
|
||||
httpResponse.PlainTextHandler(w, http.StatusNoContent, "")
|
||||
}
|
||||
}
|
||||
func polkaWebhookHandler(cfg *apiConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
key, err := auth.GetAPIKey(r.Header)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
if key != cfg.polkaKey {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
type polkaWebhook struct {
|
||||
Event string `json:"event"`
|
||||
Data struct {
|
||||
UserId string `json:"user_id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
data := polkaWebhook{}
|
||||
err = json.NewDecoder(r.Body).Decode(&data)
|
||||
if err != nil {
|
||||
httpResponse.SomethingWentWrong(w)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Event != "user.upgraded" {
|
||||
httpResponse.JSONHandler(w, http.StatusNoContent, "")
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(data.Data.UserId)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusBadRequest, `{"error": "InvalidUserID"}`)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = cfg.db.GetUserByID(context.Background(), userID)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusNotFound, `{"error": "UserNotFound"}`)
|
||||
return
|
||||
}
|
||||
|
||||
err = cfg.db.UpgradeUserToChirpyRed(context.Background(), userID)
|
||||
if err != nil {
|
||||
httpResponse.SomethingWentWrong(w)
|
||||
return
|
||||
}
|
||||
httpResponse.PlainTextHandler(w, http.StatusNoContent, "")
|
||||
}
|
||||
}
|
||||
|
||||
func updateUserDatahandler(cfg *apiConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
type parameters struct {
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
token, err := auth.GetBearerToken(r.Header)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := auth.ValidateJWT(token, cfg.secret)
|
||||
|
||||
_, err = cfg.db.GetUserByID(context.Background(), userId)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
params := parameters{}
|
||||
err = json.NewDecoder(r.Body).Decode(¶ms)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusBadRequest, `{"error": "Failed to parse request body"}`)
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := auth.HashPassword(params.Password)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusInternalServerError, `{"error": "Failed to hash password"}`)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := cfg.db.UpdateUserEmailAndPassword(context.Background(), database.UpdateUserEmailAndPasswordParams{
|
||||
Email: params.Email,
|
||||
HashedPassword: hashedPassword,
|
||||
ID: userId,
|
||||
})
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "%s"}`, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
userJSON := dataMaps.User{
|
||||
ID: user.ID,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
Email: user.Email,
|
||||
IsChirpyRed: user.IsChirpyRed,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(userJSON)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "%s"}`, err.Error()))
|
||||
return
|
||||
}
|
||||
httpResponse.JSONHandler(w, http.StatusOK, string(jsonData))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func postChirpsHandler(cfg *apiConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
type parameters struct {
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
token, err := auth.GetBearerToken(r.Header)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := auth.ValidateJWT(token, cfg.secret)
|
||||
|
||||
params := parameters{}
|
||||
err = json.NewDecoder(r.Body).Decode(¶ms)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusBadRequest, `{"error": "Failed to parse request body"}`)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = cfg.db.GetUserByID(context.Background(), userId)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
chirp, err := validateChirp(params.Body)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusBadRequest, fmt.Sprintf(`{"error": "%s"}`, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
newChirp, err := cfg.db.CreateChirp(context.Background(), database.CreateChirpParams{
|
||||
Body: chirp,
|
||||
UserID: userId,
|
||||
})
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "%s"}`, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
chirpJSON := dataMaps.Chirp{
|
||||
ID: newChirp.ID,
|
||||
CreatedAt: newChirp.CreatedAt,
|
||||
UpdatedAt: newChirp.UpdatedAt,
|
||||
Body: newChirp.Body,
|
||||
UserID: newChirp.UserID,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(chirpJSON)
|
||||
if err != nil {
|
||||
httpResponse.SomethingWentWrong(w)
|
||||
return
|
||||
}
|
||||
httpResponse.JSONHandler(w, http.StatusCreated, string(jsonData))
|
||||
}
|
||||
}
|
||||
|
||||
func refreshTokenHandler(cfg *apiConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
token, err := auth.GetBearerToken(r.Header)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
find, err := cfg.db.GetRefreshTokenByToken(context.Background(), token)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = cfg.db.GetUserByID(context.Background(), find.UserID)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
if find.RevokedAt.Valid {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
newToken, err := auth.MakeJWT(find.UserID, cfg.secret, time.Duration(3600)*time.Second)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "%s"}`, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
httpResponse.JSONHandler(w, http.StatusOK, fmt.Sprintf(`{"token": "%s"}`, newToken))
|
||||
}
|
||||
}
|
||||
|
||||
func revokeRefreshTokenHandler(cfg *apiConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
token, err := auth.GetBearerToken(r.Header)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
find, err := cfg.db.GetRefreshTokenByToken(context.Background(), token)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Unauthorized"}`)
|
||||
return
|
||||
}
|
||||
|
||||
err = cfg.db.RevokeRefreshTokenByToken(context.Background(), find.Token)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "%s"}`, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
httpResponse.JSONHandler(w, http.StatusNoContent, "")
|
||||
}
|
||||
}
|
||||
|
||||
func loginHandler(cfg *apiConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
type parameters struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
params := parameters{}
|
||||
@@ -87,52 +466,119 @@ func postUsersHandler(cfg *apiConfig) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
usr, err := cfg.db.CreateUser(context.Background(), params.Email)
|
||||
user, err := cfg.db.GetUserByEmail(context.Background(), params.Email)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusNotFound, `{"error": "UserNotFound"}`)
|
||||
return
|
||||
}
|
||||
|
||||
err = auth.CheckPasswordHash(params.Password, user.HashedPassword)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusUnauthorized, `{"error": "Incorrect email or password"}`)
|
||||
return
|
||||
}
|
||||
|
||||
userJSON := dataMaps.UserWithToken{
|
||||
ID: user.ID,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
Email: user.Email,
|
||||
IsChirpyRed: user.IsChirpyRed,
|
||||
}
|
||||
|
||||
expiresIn := time.Duration(3600) * time.Second
|
||||
|
||||
userJSON.Token, err = auth.MakeJWT(user.ID, cfg.secret, expiresIn)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusBadRequest, fmt.Sprintf(`{"error": "%s"}`, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
userJSON.RefreshToken, err = auth.MakeRefreshToken()
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "%s"}`, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = cfg.db.CreateRefreshToken(context.Background(), database.CreateRefreshTokenParams{
|
||||
Token: userJSON.RefreshToken,
|
||||
UserID: userJSON.ID,
|
||||
ExpiresAt: time.Now().UTC().Add(time.Duration(60*24*3600) * time.Second),
|
||||
})
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "%s"}`, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(userJSON)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "%s"}`, err.Error()))
|
||||
return
|
||||
}
|
||||
httpResponse.JSONHandler(w, http.StatusOK, string(jsonData))
|
||||
}
|
||||
}
|
||||
|
||||
func postUsersHandler(cfg *apiConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
type parameters struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
params := parameters{}
|
||||
err := json.NewDecoder(r.Body).Decode(¶ms)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusBadRequest, `{"error": "Failed to parse request body"}`)
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := auth.HashPassword(params.Password)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "%s"}`, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
usr, err := cfg.db.CreateUser(context.Background(), database.CreateUserParams{
|
||||
Email: params.Email,
|
||||
HashedPassword: hash,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "Failed to create user", "message": "%s"}`, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
jsonData := fmt.Sprintf(`{
|
||||
"id": "%s",
|
||||
"created_at": "%s",
|
||||
"updated_at": "%s",
|
||||
"email": "%s"
|
||||
}`, usr.ID, usr.CreatedAt, usr.UpdatedAt, usr.Email)
|
||||
httpResponse.JSONHandler(w, http.StatusCreated, jsonData)
|
||||
userJSON := dataMaps.User{
|
||||
ID: usr.ID,
|
||||
CreatedAt: usr.CreatedAt,
|
||||
UpdatedAt: usr.UpdatedAt,
|
||||
Email: usr.Email,
|
||||
IsChirpyRed: usr.IsChirpyRed,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(userJSON)
|
||||
if err != nil {
|
||||
httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "%s"}`, err.Error()))
|
||||
return
|
||||
}
|
||||
httpResponse.JSONHandler(w, http.StatusCreated, string(jsonData))
|
||||
}
|
||||
}
|
||||
|
||||
func validateChirpHandler(w http.ResponseWriter, r *http.Request) {
|
||||
type parameters struct {
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
func validateChirp(chirp string) (string, error) {
|
||||
profaneWords := []string{"kerfuffle", "sharbert", "fornax"}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
params := parameters{}
|
||||
err := decoder.Decode(¶ms)
|
||||
if err != nil {
|
||||
httpResponse.SomethingWentWrong(w)
|
||||
return
|
||||
if len(chirp) > 140 {
|
||||
return "", fmt.Errorf("chirp is too long")
|
||||
}
|
||||
|
||||
if len(params.Body) > 140 {
|
||||
httpResponse.JSONHandler(w, http.StatusBadRequest, `{"error": "Chirp is too long"}`)
|
||||
return
|
||||
}
|
||||
|
||||
chirp := params.Body
|
||||
|
||||
for _, word := range profaneWords {
|
||||
// Use word boundaries (\b) to match whole words and ignore punctuation
|
||||
re := regexp.MustCompile(`(?i)\b` + regexp.QuoteMeta(word) + `\b`)
|
||||
chirp = re.ReplaceAllString(chirp, "****")
|
||||
}
|
||||
|
||||
httpResponse.JSONHandler(w, http.StatusOK, fmt.Sprintf(`{"cleaned_body": "%s"}`, chirp))
|
||||
return
|
||||
return chirp, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -145,6 +591,8 @@ func main() {
|
||||
dbURL := os.Getenv("DB_URL")
|
||||
|
||||
apiCfg.platform = os.Getenv("PLATFORM")
|
||||
apiCfg.secret = os.Getenv("SECRET")
|
||||
apiCfg.polkaKey = os.Getenv("POLKA_KEY")
|
||||
|
||||
db, err := sql.Open("postgres", dbURL)
|
||||
if err != nil {
|
||||
@@ -159,8 +607,16 @@ func main() {
|
||||
mux.HandleFunc("GET /api/healthz", readinessHandler)
|
||||
mux.HandleFunc("GET /api/metrics", metricsHandler(apiCfg))
|
||||
mux.HandleFunc("POST /api/reset", resetMetricsHandler(apiCfg))
|
||||
mux.HandleFunc("POST /api/validate_chirp", validateChirpHandler)
|
||||
mux.HandleFunc("POST /api/users", postUsersHandler(apiCfg))
|
||||
mux.HandleFunc("POST /api/login", loginHandler(apiCfg))
|
||||
mux.HandleFunc("POST /api/refresh", refreshTokenHandler(apiCfg))
|
||||
mux.HandleFunc("POST /api/revoke", revokeRefreshTokenHandler(apiCfg))
|
||||
mux.HandleFunc("POST /api/chirps", postChirpsHandler(apiCfg))
|
||||
mux.HandleFunc("GET /api/chirps", getAllChirpsHandler(apiCfg))
|
||||
mux.HandleFunc("GET /api/chirps/{chirpID}", getChirpByIDHandler(apiCfg))
|
||||
mux.HandleFunc("DELETE /api/chirps/{chirpID}", deleteChirpByIDHandler(apiCfg))
|
||||
mux.HandleFunc("POST /api/polka/webhooks", polkaWebhookHandler(apiCfg))
|
||||
mux.HandleFunc("PUT /api/users", updateUserDatahandler(apiCfg))
|
||||
|
||||
mux.HandleFunc("GET /admin/metrics", adminMetricsHandler(apiCfg))
|
||||
mux.HandleFunc("POST /admin/reset", resetMetricsHandler(apiCfg))
|
||||
|
||||
4
sql/queries/CreateChirp.sql
Normal file
4
sql/queries/CreateChirp.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- name: CreateChirp :one
|
||||
INSERT INTO chirps (id, created_at, updated_at, body, user_id)
|
||||
VALUES (gen_random_uuid(), NOW(), NOW(), $1, $2)
|
||||
RETURNING *;
|
||||
4
sql/queries/CreateRefreshToken.sql
Normal file
4
sql/queries/CreateRefreshToken.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- name: CreateRefreshToken :one
|
||||
INSERT INTO refresh_tokens (token, created_at, updated_at, user_id, expires_at, revoked_at)
|
||||
VALUES ($1, NOW(), NOW(), $2, $3, null)
|
||||
RETURNING *;
|
||||
@@ -1,4 +1,4 @@
|
||||
-- name: CreateUser :one
|
||||
INSERT INTO users (id, created_at, updated_at, email)
|
||||
VALUES (gen_random_uuid(), NOW(), NOW(), $1)
|
||||
INSERT INTO users (id, created_at, updated_at, email, hashed_password)
|
||||
VALUES (gen_random_uuid(), NOW(), NOW(), $1, $2)
|
||||
RETURNING *;
|
||||
2
sql/queries/DeleteChirpByID.sql
Normal file
2
sql/queries/DeleteChirpByID.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- name: DeleteChirpByID :exec
|
||||
DELETE FROM chirps WHERE id = $1;
|
||||
2
sql/queries/GetAllChirps.sql
Normal file
2
sql/queries/GetAllChirps.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- name: GetAllChirps :many
|
||||
SELECT * FROM chirps ORDER BY created_at;
|
||||
2
sql/queries/GetChirpByID.sql
Normal file
2
sql/queries/GetChirpByID.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- name: GetChirpByID :one
|
||||
SELECT * FROM chirps WHERE id = $1;
|
||||
2
sql/queries/GetChirpsByUserID.sql
Normal file
2
sql/queries/GetChirpsByUserID.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- name: GetChirpsByUserID :many
|
||||
SELECT * FROM chirps WHERE user_id = $1 ORDER BY created_at;
|
||||
2
sql/queries/GetRefreshTokenByToken.sql
Normal file
2
sql/queries/GetRefreshTokenByToken.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- name: GetRefreshTokenByToken :one
|
||||
SELECT * FROM refresh_tokens WHERE token = $1 AND revoked_at is null;
|
||||
2
sql/queries/GetUserByEmail.sql
Normal file
2
sql/queries/GetUserByEmail.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- name: GetUserByEmail :one
|
||||
SELECT * FROM users WHERE email = $1;
|
||||
2
sql/queries/GetUserByID.sql
Normal file
2
sql/queries/GetUserByID.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- name: GetUserByID :one
|
||||
SELECT * FROM users WHERE id = $1;
|
||||
2
sql/queries/RevokeRefreshTokenByToken.sql
Normal file
2
sql/queries/RevokeRefreshTokenByToken.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- name: RevokeRefreshTokenByToken :exec
|
||||
UPDATE refresh_tokens SET revoked_at = CURRENT_TIMESTAMP WHERE token = $1;
|
||||
3
sql/queries/UpdateUserEmailAndPassword.sql
Normal file
3
sql/queries/UpdateUserEmailAndPassword.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- name: UpdateUserEmailAndPassword :one
|
||||
UPDATE users SET email = $1, hashed_password = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $3
|
||||
RETURNING *;
|
||||
3
sql/queries/UpgradeUserToChirpyRed.sql
Normal file
3
sql/queries/UpgradeUserToChirpyRed.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- name: UpgradeUserToChirpyRed :exec
|
||||
UPDATE users SET is_chirpy_red = true WHERE id = $1
|
||||
RETURNING *;
|
||||
11
sql/schema/002_chirps.sql
Normal file
11
sql/schema/002_chirps.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- +goose Up
|
||||
CREATE TABLE chirps(
|
||||
id UUID PRIMARY KEY,
|
||||
created_at TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP,
|
||||
body TEXT not null UNIQUE,
|
||||
user_id UUID not null REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE chirps;
|
||||
5
sql/schema/003_app_hashed_password_to_users_table.sql
Normal file
5
sql/schema/003_app_hashed_password_to_users_table.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE users ADD COLUMN hashed_password TEXT NOT NULL DEFAULT 'unset';
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE users DROP COLUMN hashed_password;
|
||||
12
sql/schema/004_refresh_tokens.sql
Normal file
12
sql/schema/004_refresh_tokens.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- +goose Up
|
||||
CREATE TABLE refresh_tokens(
|
||||
token TEXT PRIMARY KEY,
|
||||
created_at TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP not null DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id UUID not null REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires_at timestamp not null,
|
||||
revoked_at timestamp default null
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE refresh_tokens;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE users ADD COLUMN is_chirpy_red BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE users DROP COLUMN is_chirpy_red;
|
||||
Reference in New Issue
Block a user