Skip to content
This repository has been archived by the owner on Jun 2, 2024. It is now read-only.

Commit

Permalink
Add global and private feed endpoints (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
Frecherenkel60 authored Jan 17, 2024
1 parent 2f3804c commit bf85f00
Show file tree
Hide file tree
Showing 11 changed files with 557 additions and 278 deletions.
16 changes: 8 additions & 8 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Scripts owned by Luca (@930C)
/scripts/* @930C
/scripts/** @930C

# GitHub related files owned by Kevin (@Frecherenkel60)
.github/* @Frecherenkel60
.github/** @Frecherenkel60

# Deployments and Build owned by Luca (@930C) and Keivn (@Frecherenkel60)
/deployments/* @930C @Frecherenkel60
/build/* @930C @Frecherenkel60
/build/** @930C @Frecherenkel60

# Everything else owned by project team (@wwi21seb-projekt/server-alpha)
/api/* @wwi21seb-projekt/server-alpha
/cmd/* @wwi21seb-projekt/server-alpha
/configs/* @wwi21seb-projekt/server-alpha
/docs/* @wwi21seb-projekt/server-alpha
/internal/* @wwi21seb-projekt/server-alpha
/api/** @wwi21seb-projekt/server-alpha
/cmd/** @wwi21seb-projekt/server-alpha
/configs/** @wwi21seb-projekt/server-alpha
/docs/** @wwi21seb-projekt/server-alpha
/internal/** @wwi21seb-projekt/server-alpha
16 changes: 8 additions & 8 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
| [Trello Ticket 1] | <span style="color:yellow">POST</span> | `/users/login` | Login ||
| [Trello Ticket 2] | <span style="color:green">GET</span> | `/imprint` | Impressum ||
| [Trello Ticket 3.1], [Trello Ticket 3.2] | <span style="color:yellow">POST</span> | `/posts` | Create Posts ||
| [Trello Ticket 5] | <span style="color:green">GET</span> | `/users/:username` | Visit User Profile | |
| [Trello Ticket 4] | <span style="color:green">GET</span> | `/users?username&offset&limit` | Search User | |
| [Trello Ticket 5] | <span style="color:green">GET</span> | `/users/:username` | Visit User Profile | |
| [Trello Ticket 4] | <span style="color:green">GET</span> | `/users?username&offset&limit` | Search User | |
| [Trello Ticket 4] | <span style="color:green">GET</span> | `/users/:username/feed?offset&limit` | User Feed ||
| [Trello Ticket 4] | <span style="color:yellow">POST</span> | `/subscriptions` | Subscribe User | |
| [Trello Ticket 4] | <span style="color:red">DELETE</span> | `/subscriptions/:subscriptionId` | Unsubscribe User | |
| [Trello Ticket 6.1] | <span style="color:green">GET</span> | `/feed?postId&limit&feedType` | Get own or global feed | |
| [Trello Ticket 6.2] | <span style="color:green">GET</span> | `/feed?postId&limit` | Get global feed (no auth) | |
| [Trello Ticket 7] | <span style="color:blue">PUT</span> | `/users` | Change trivial information | |
| [Trello Ticket 7] | <span style="color:purple">PATCH</span> | `/users` | Change password | |
| [Trello Ticket 4] | <span style="color:yellow">POST</span> | `/subscriptions` | Subscribe User | |
| [Trello Ticket 4] | <span style="color:red">DELETE</span> | `/subscriptions/:subscriptionId` | Unsubscribe User | |
| [Trello Ticket 6.1] | <span style="color:green">GET</span> | `/feed?postId&limit&feedType` | Get own or global feed | |
| [Trello Ticket 6.2] | <span style="color:green">GET</span> | `/feed?postId&limit` | Get global feed (no auth) | |
| [Trello Ticket 7] | <span style="color:blue">PUT</span> | `/users` | Change trivial information | |
| [Trello Ticket 7] | <span style="color:purple">PATCH</span> | `/users` | Change password | |

[Trello Ticket 1]: https://trello.com/c/1w0QP6u5/209-id-0-als-nutzer-möchte-ich-mich-mit-email-und-passwort-registrieren-können-um-einen-gesicherten-zugang-zu-meinem-account-zu-habe

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.21
require (
github.com/gavv/httpexpect/v2 v2.16.0
github.com/go-chi/chi/v5 v5.0.11
github.com/go-chi/cors v1.2.1
github.com/go-playground/validator/v10 v10.17.0
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.5.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ github.com/gavv/httpexpect/v2 v2.16.0/go.mod h1:uJLaO+hQ25ukBJtQi750PsztObHybNll
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
Expand Down
138 changes: 76 additions & 62 deletions internal/managers/jwt_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ import (
)

type JWTMgr interface {
GenerateJWT(claims jwt.Claims) (string, error)
GenerateJWT(userId, username string, isRefreshToken bool) (string, error)
ValidateJWT(tokenString string) (jwt.Claims, error)
JWTMiddleware(next http.Handler) http.Handler
GenerateClaims(userId, username string) jwt.Claims
}

// JWTManager handles JWT generation, signing, and validation.
Expand All @@ -43,6 +42,67 @@ func NewJWTManager(privateKey ed25519.PrivateKey, publicKey ed25519.PublicKey) J
return &JWTManager
}

// GenerateJWT generates a new JWT with the given claims.
func (jm *JWTManager) GenerateJWT(userId, username string, isRefreshToken bool) (string, error) {
claims := generateClaims(userId, username, isRefreshToken)

token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
return token.SignedString(jm.privateKey)
}

// ValidateJWT validates the given JWT and returns the claims if valid.
func (jm *JWTManager) ValidateJWT(tokenString string) (jwt.Claims, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Verify the signing method
if token.Method.Alg() != jwt.SigningMethodEdDSA.Alg() {
return nil, fmt.Errorf("invalid signing method")
}

return jm.publicKey, nil
})

if err != nil {
return nil, err
}

if !token.Valid {
return nil, jwt.ErrSignatureInvalid
}

return token.Claims, nil
}

// JWTMiddleware is a middleware that validates the JWT token in the request header.
func (jm *JWTManager) JWTMiddleware(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// Check if the request has a JWT token
if r.Header.Get("Authorization") == "" {
utils.WriteAndLogError(w, schemas.Unauthorized, http.StatusUnauthorized, fmt.Errorf("missing authorization header"))
return
}

// Extract the JWT token from the request header
header := r.Header.Get("Authorization")
token := header[len("Bearer "):]

// Validate the JWT token
claims, err := jm.ValidateJWT(token)
if err != nil || claims.(jwt.MapClaims)["refresh"] == "true" {
utils.WriteAndLogError(w, schemas.Unauthorized, http.StatusUnauthorized, err)
return
}

// Add the claims to the request context
ctx := r.Context()
ctx = context.WithValue(ctx, utils.ClaimsKey, claims)
r = r.WithContext(ctx)

next.ServeHTTP(w, r)
}

return http.HandlerFunc(fn)
}

func NewJWTManagerFromFile() (JWTMgr, error) {
log.Info("Initializing JWT manager using key pair from file...")
privateKeyPath := "private_key.pem"
Expand Down Expand Up @@ -201,70 +261,24 @@ func generateAndStoreKeys(privateKeyPath, publicKeyPath string) error {
}

// GenerateClaims generates the standard JWT claims.
func (jm *JWTManager) GenerateClaims(userId, username string) jwt.Claims {
func generateClaims(userId, username string, isRefreshToken bool) jwt.Claims {
var exp int64
var refresh string

if isRefreshToken {
exp = time.Now().Add(time.Hour * 24 * 7).Unix()
refresh = "true"
} else {
exp = time.Now().Add(time.Hour * 24).Unix()
refresh = "false"
}

return jwt.MapClaims{
"iss": "server-alpha.tech",
"iat": time.Now().Unix(),
"exp": time.Now().Add(time.Hour * 24).Unix(),
"exp": exp,
"sub": userId,
"username": username,
"refresh": refresh,
}
}

// GenerateJWT generates a new JWT with the given claims.
func (jm *JWTManager) GenerateJWT(claims jwt.Claims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
return token.SignedString(jm.privateKey)
}

// ValidateJWT validates the given JWT and returns the claims if valid.
func (jm *JWTManager) ValidateJWT(tokenString string) (jwt.Claims, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Verify the signing method
if token.Method.Alg() != jwt.SigningMethodEdDSA.Alg() {
return nil, fmt.Errorf("invalid signing method")
}

return jm.publicKey, nil
})

if err != nil {
return nil, err
}

if !token.Valid {
return nil, jwt.ErrSignatureInvalid
}

return token.Claims, nil
}

func (jm *JWTManager) JWTMiddleware(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// Check if the request has a JWT token
if r.Header.Get("Authorization") == "" {
utils.WriteAndLogError(w, schemas.Unauthorized, http.StatusUnauthorized, fmt.Errorf("missing authorization header"))
return
}

// Extract the JWT token from the request header
header := r.Header.Get("Authorization")
token := header[len("Bearer "):]

// Validate the JWT token
claims, err := jm.ValidateJWT(token)
if err != nil {
utils.WriteAndLogError(w, schemas.Unauthorized, http.StatusUnauthorized, err)
return
}

// Add the claims to the request context
ctx := r.Context()
ctx = context.WithValue(ctx, utils.ClaimsKey, claims)
r = r.WithContext(ctx)

next.ServeHTTP(w, r)
}

return http.HandlerFunc(fn)
}
Loading

0 comments on commit bf85f00

Please sign in to comment.