Compare commits
10 Commits
5618aeb439
...
0e5c409621
| 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
|
go 1.23.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
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
|
const createUser = `-- name: CreateUser :one
|
||||||
INSERT INTO users (id, created_at, updated_at, email)
|
INSERT INTO users (id, created_at, updated_at, email, hashed_password)
|
||||||
VALUES (gen_random_uuid(), NOW(), NOW(), $1)
|
VALUES (gen_random_uuid(), NOW(), NOW(), $1, $2)
|
||||||
RETURNING id, created_at, updated_at, email
|
RETURNING id, created_at, updated_at, email, hashed_password, is_chirpy_red
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) CreateUser(ctx context.Context, email string) (User, error) {
|
type CreateUserParams struct {
|
||||||
row := q.db.QueryRowContext(ctx, createUser, email)
|
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
|
var i User
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.Email,
|
&i.Email,
|
||||||
|
&i.HashedPassword,
|
||||||
|
&i.IsChirpyRed,
|
||||||
)
|
)
|
||||||
return i, err
|
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
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"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 {
|
type User struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
Email string
|
Email string
|
||||||
|
HashedPassword string
|
||||||
|
IsChirpyRed bool
|
||||||
}
|
}
|
||||||
|
|||||||
518
main.go
518
main.go
@@ -5,21 +5,28 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
_ "github.com/lib/pq"
|
_ "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/database"
|
||||||
"github.com/rdarius/go-http-server/internal/httpResponse"
|
"github.com/rdarius/go-http-server/internal/httpResponse"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type apiConfig struct {
|
type apiConfig struct {
|
||||||
fileserverHits atomic.Int32
|
fileserverHits atomic.Int32
|
||||||
db database.Queries
|
db database.Queries
|
||||||
platform string
|
platform string
|
||||||
|
secret string
|
||||||
|
polkaKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *apiConfig) middlewareMetricsInc(next http.Handler) http.Handler {
|
func (cfg *apiConfig) middlewareMetricsInc(next http.Handler) http.Handler {
|
||||||
@@ -74,10 +81,382 @@ func fileServerHandler() http.Handler {
|
|||||||
return http.StripPrefix("/app", fileServer)
|
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) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
type parameters struct {
|
type parameters struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
params := parameters{}
|
params := parameters{}
|
||||||
@@ -87,52 +466,119 @@ func postUsersHandler(cfg *apiConfig) http.HandlerFunc {
|
|||||||
return
|
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 {
|
if err != nil {
|
||||||
httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "Failed to create user", "message": "%s"}`, err.Error()))
|
httpResponse.JSONHandler(w, http.StatusInternalServerError, fmt.Sprintf(`{"error": "Failed to create user", "message": "%s"}`, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonData := fmt.Sprintf(`{
|
userJSON := dataMaps.User{
|
||||||
"id": "%s",
|
ID: usr.ID,
|
||||||
"created_at": "%s",
|
CreatedAt: usr.CreatedAt,
|
||||||
"updated_at": "%s",
|
UpdatedAt: usr.UpdatedAt,
|
||||||
"email": "%s"
|
Email: usr.Email,
|
||||||
}`, usr.ID, usr.CreatedAt, usr.UpdatedAt, usr.Email)
|
IsChirpyRed: usr.IsChirpyRed,
|
||||||
httpResponse.JSONHandler(w, http.StatusCreated, jsonData)
|
}
|
||||||
|
|
||||||
|
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) {
|
func validateChirp(chirp string) (string, error) {
|
||||||
type parameters struct {
|
|
||||||
Body string `json:"body"`
|
|
||||||
}
|
|
||||||
|
|
||||||
profaneWords := []string{"kerfuffle", "sharbert", "fornax"}
|
profaneWords := []string{"kerfuffle", "sharbert", "fornax"}
|
||||||
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
if len(chirp) > 140 {
|
||||||
params := parameters{}
|
return "", fmt.Errorf("chirp is too long")
|
||||||
err := decoder.Decode(¶ms)
|
|
||||||
if err != nil {
|
|
||||||
httpResponse.SomethingWentWrong(w)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(params.Body) > 140 {
|
|
||||||
httpResponse.JSONHandler(w, http.StatusBadRequest, `{"error": "Chirp is too long"}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
chirp := params.Body
|
|
||||||
|
|
||||||
for _, word := range profaneWords {
|
for _, word := range profaneWords {
|
||||||
// Use word boundaries (\b) to match whole words and ignore punctuation
|
|
||||||
re := regexp.MustCompile(`(?i)\b` + regexp.QuoteMeta(word) + `\b`)
|
re := regexp.MustCompile(`(?i)\b` + regexp.QuoteMeta(word) + `\b`)
|
||||||
chirp = re.ReplaceAllString(chirp, "****")
|
chirp = re.ReplaceAllString(chirp, "****")
|
||||||
}
|
}
|
||||||
|
|
||||||
httpResponse.JSONHandler(w, http.StatusOK, fmt.Sprintf(`{"cleaned_body": "%s"}`, chirp))
|
return chirp, nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -145,6 +591,8 @@ func main() {
|
|||||||
dbURL := os.Getenv("DB_URL")
|
dbURL := os.Getenv("DB_URL")
|
||||||
|
|
||||||
apiCfg.platform = os.Getenv("PLATFORM")
|
apiCfg.platform = os.Getenv("PLATFORM")
|
||||||
|
apiCfg.secret = os.Getenv("SECRET")
|
||||||
|
apiCfg.polkaKey = os.Getenv("POLKA_KEY")
|
||||||
|
|
||||||
db, err := sql.Open("postgres", dbURL)
|
db, err := sql.Open("postgres", dbURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -159,8 +607,16 @@ func main() {
|
|||||||
mux.HandleFunc("GET /api/healthz", readinessHandler)
|
mux.HandleFunc("GET /api/healthz", readinessHandler)
|
||||||
mux.HandleFunc("GET /api/metrics", metricsHandler(apiCfg))
|
mux.HandleFunc("GET /api/metrics", metricsHandler(apiCfg))
|
||||||
mux.HandleFunc("POST /api/reset", resetMetricsHandler(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/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("GET /admin/metrics", adminMetricsHandler(apiCfg))
|
||||||
mux.HandleFunc("POST /admin/reset", resetMetricsHandler(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
|
-- name: CreateUser :one
|
||||||
INSERT INTO users (id, created_at, updated_at, email)
|
INSERT INTO users (id, created_at, updated_at, email, hashed_password)
|
||||||
VALUES (gen_random_uuid(), NOW(), NOW(), $1)
|
VALUES (gen_random_uuid(), NOW(), NOW(), $1, $2)
|
||||||
RETURNING *;
|
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