arch: move web folder into api & move api to top level
This commit is contained in:
		
							
								
								
									
										166
									
								
								internal/route/auth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								internal/route/auth.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,166 @@
 | 
			
		||||
package route
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/dgrijalva/jwt-go"
 | 
			
		||||
	"github.com/go-chi/chi"
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	"github.com/jordanknott/project-citadel/api/internal/auth"
 | 
			
		||||
	"github.com/jordanknott/project-citadel/api/internal/db"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var jwtKey = []byte("citadel_test_key")
 | 
			
		||||
 | 
			
		||||
type authResource struct{}
 | 
			
		||||
 | 
			
		||||
type AccessTokenClaims struct {
 | 
			
		||||
	UserID string `json:"userId"`
 | 
			
		||||
	jwt.StandardClaims
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RefreshTokenClaims struct {
 | 
			
		||||
	UserID string `json:"userId"`
 | 
			
		||||
	jwt.StandardClaims
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LoginRequestData struct {
 | 
			
		||||
	Username string
 | 
			
		||||
	Password string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LoginResponseData struct {
 | 
			
		||||
	AccessToken string `json:"accessToken"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LogoutResponseData struct {
 | 
			
		||||
	Status string `json:"status"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RefreshTokenResponseData struct {
 | 
			
		||||
	AccessToken string `json:"accessToken"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AvatarUploadResponseData struct {
 | 
			
		||||
	UserID string `json:"userID"`
 | 
			
		||||
	URL    string `json:"url"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *CitadelHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	c, err := r.Cookie("refreshToken")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if err == http.ErrNoCookie {
 | 
			
		||||
			w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	refreshTokenID := uuid.MustParse(c.Value)
 | 
			
		||||
	token, err := h.repo.GetRefreshTokenByID(r.Context(), refreshTokenID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	refreshCreatedAt := time.Now().UTC()
 | 
			
		||||
	refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
 | 
			
		||||
	refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{token.UserID, refreshCreatedAt, refreshExpiresAt})
 | 
			
		||||
 | 
			
		||||
	err = h.repo.DeleteRefreshTokenByID(r.Context(), token.TokenID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	accessTokenString, err := auth.NewAccessToken(token.UserID.String())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.Header().Set("Content-type", "application/json")
 | 
			
		||||
	http.SetCookie(w, &http.Cookie{
 | 
			
		||||
		Name:     "refreshToken",
 | 
			
		||||
		Value:    refreshTokenString.TokenID.String(),
 | 
			
		||||
		Expires:  refreshExpiresAt,
 | 
			
		||||
		HttpOnly: true,
 | 
			
		||||
	})
 | 
			
		||||
	json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *CitadelHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	c, err := r.Cookie("refreshToken")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if err == http.ErrNoCookie {
 | 
			
		||||
			w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	refreshTokenID := uuid.MustParse(c.Value)
 | 
			
		||||
	err = h.repo.DeleteRefreshTokenByID(r.Context(), refreshTokenID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	json.NewEncoder(w).Encode(LogoutResponseData{Status: "success"})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *CitadelHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	var requestData LoginRequestData
 | 
			
		||||
	err := json.NewDecoder(r.Body).Decode(&requestData)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
		log.Debug("bad request body")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := h.repo.GetUserAccountByUsername(r.Context(), requestData.Username)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithFields(log.Fields{
 | 
			
		||||
			"username": requestData.Username,
 | 
			
		||||
		}).Warn("user account not found")
 | 
			
		||||
		w.WriteHeader(http.StatusUnauthorized)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(requestData.Password))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithFields(log.Fields{
 | 
			
		||||
			"password":      requestData.Password,
 | 
			
		||||
			"password_hash": user.PasswordHash,
 | 
			
		||||
		}).Warn("password incorrect")
 | 
			
		||||
		w.WriteHeader(http.StatusUnauthorized)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	refreshCreatedAt := time.Now().UTC()
 | 
			
		||||
	refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
 | 
			
		||||
	refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
 | 
			
		||||
 | 
			
		||||
	accessTokenString, err := auth.NewAccessToken(user.UserID.String())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.Header().Set("Content-type", "application/json")
 | 
			
		||||
	http.SetCookie(w, &http.Cookie{
 | 
			
		||||
		Name:     "refreshToken",
 | 
			
		||||
		Value:    refreshTokenString.TokenID.String(),
 | 
			
		||||
		Expires:  refreshExpiresAt,
 | 
			
		||||
		HttpOnly: true,
 | 
			
		||||
	})
 | 
			
		||||
	json.NewEncoder(w).Encode(LoginResponseData{accessTokenString})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (rs authResource) Routes(citadelHandler CitadelHandler) chi.Router {
 | 
			
		||||
	r := chi.NewRouter()
 | 
			
		||||
	r.Post("/login", citadelHandler.LoginHandler)
 | 
			
		||||
	r.Post("/refresh_token", citadelHandler.RefreshTokenHandler)
 | 
			
		||||
	r.Post("/logout", citadelHandler.LogoutHandler)
 | 
			
		||||
	return r
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										52
									
								
								internal/route/avatar.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								internal/route/avatar.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
package route
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
 | 
			
		||||
	"github.com/jordanknott/project-citadel/api/internal/db"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (h *CitadelHandler) ProfileImageUpload(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	log.Info("preparing to upload file")
 | 
			
		||||
	userID, ok := r.Context().Value("userID").(uuid.UUID)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		log.Error("not a valid uuid")
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Parse our multipart form, 10 << 20 specifies a maximum
 | 
			
		||||
	// upload of 10 MB files.
 | 
			
		||||
	r.ParseMultipartForm(10 << 20)
 | 
			
		||||
 | 
			
		||||
	file, handler, err := r.FormFile("file")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Error("issue while uploading file")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer file.Close()
 | 
			
		||||
	log.WithFields(log.Fields{"filename": handler.Filename, "size": handler.Size, "header": handler.Header}).Info("file metadata")
 | 
			
		||||
 | 
			
		||||
	fileBytes, err := ioutil.ReadAll(file)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Error("while reading file")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	err = ioutil.WriteFile("uploads/"+handler.Filename, fileBytes, 0644)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Error("while reading file")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.repo.UpdateUserAccountProfileAvatarURL(r.Context(), db.UpdateUserAccountProfileAvatarURLParams{UserID: userID, ProfileAvatarUrl: sql.NullString{String: "http://localhost:3333/uploads/" + handler.Filename, Valid: true}})
 | 
			
		||||
	// return that we have successfully uploaded our file!
 | 
			
		||||
	log.Info("file uploaded")
 | 
			
		||||
	json.NewEncoder(w).Encode(AvatarUploadResponseData{URL: "http://localhost:3333/uploads/" + handler.Filename, UserID: userID.String()})
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								internal/route/middleware.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								internal/route/middleware.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
package route
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	"github.com/jordanknott/project-citadel/api/internal/auth"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func AuthenticationMiddleware(next http.Handler) http.Handler {
 | 
			
		||||
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		bearerTokenRaw := r.Header.Get("Authorization")
 | 
			
		||||
		splitToken := strings.Split(bearerTokenRaw, "Bearer")
 | 
			
		||||
		if len(splitToken) != 2 {
 | 
			
		||||
			w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		accessTokenString := strings.TrimSpace(splitToken[1])
 | 
			
		||||
		accessClaims, err := auth.ValidateAccessToken(accessTokenString)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if _, ok := err.(*auth.ErrExpiredToken); ok {
 | 
			
		||||
				w.WriteHeader(http.StatusUnauthorized)
 | 
			
		||||
				w.Write([]byte(`{
 | 
			
		||||
	"data": {},
 | 
			
		||||
	"errors": [
 | 
			
		||||
	{
 | 
			
		||||
		"extensions": {
 | 
			
		||||
			"code": "UNAUTHENTICATED"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	]
 | 
			
		||||
				}`))
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			log.Error(err)
 | 
			
		||||
			w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		userID, err := uuid.Parse(accessClaims.UserID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error(err)
 | 
			
		||||
			w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		ctx := context.WithValue(r.Context(), "userID", userID)
 | 
			
		||||
 | 
			
		||||
		next.ServeHTTP(w, r.WithContext(ctx))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								internal/route/route.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								internal/route/route.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
package route
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-chi/chi"
 | 
			
		||||
	"github.com/go-chi/chi/middleware"
 | 
			
		||||
	"github.com/go-chi/cors"
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
 | 
			
		||||
	"github.com/jordanknott/project-citadel/api/internal/db"
 | 
			
		||||
	"github.com/jordanknott/project-citadel/api/internal/graph"
 | 
			
		||||
	"github.com/jordanknott/project-citadel/api/internal/logger"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type CitadelHandler struct {
 | 
			
		||||
	repo db.Repository
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewRouter(dbConnection *sqlx.DB) (chi.Router, error) {
 | 
			
		||||
	formatter := new(log.TextFormatter)
 | 
			
		||||
	formatter.TimestampFormat = "02-01-2006 15:04:05"
 | 
			
		||||
	formatter.FullTimestamp = true
 | 
			
		||||
 | 
			
		||||
	routerLogger := log.New()
 | 
			
		||||
	routerLogger.SetLevel(log.InfoLevel)
 | 
			
		||||
	routerLogger.Formatter = formatter
 | 
			
		||||
	r := chi.NewRouter()
 | 
			
		||||
	cors := cors.New(cors.Options{
 | 
			
		||||
		// AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts
 | 
			
		||||
		AllowedOrigins: []string{"*"},
 | 
			
		||||
		// AllowOriginFunc:  func(r *http.Request, origin string) bool { return true },
 | 
			
		||||
		AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
 | 
			
		||||
		AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "Cookie"},
 | 
			
		||||
		ExposedHeaders:   []string{"Link"},
 | 
			
		||||
		AllowCredentials: true,
 | 
			
		||||
		MaxAge:           300, // Maximum value not ignored by any of major browsers
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	r.Use(cors.Handler)
 | 
			
		||||
	r.Use(middleware.RequestID)
 | 
			
		||||
	r.Use(middleware.RealIP)
 | 
			
		||||
	r.Use(logger.NewStructuredLogger(routerLogger))
 | 
			
		||||
	r.Use(middleware.Recoverer)
 | 
			
		||||
	r.Use(middleware.Timeout(60 * time.Second))
 | 
			
		||||
 | 
			
		||||
	repository := db.NewRepository(dbConnection)
 | 
			
		||||
	citadelHandler := CitadelHandler{*repository}
 | 
			
		||||
 | 
			
		||||
	var imgServer = http.FileServer(http.Dir("./uploads/"))
 | 
			
		||||
	r.Group(func(mux chi.Router) {
 | 
			
		||||
		mux.Mount("/auth", authResource{}.Routes(citadelHandler))
 | 
			
		||||
		mux.Handle("/__graphql", graph.NewPlaygroundHandler("/graphql"))
 | 
			
		||||
		mux.Mount("/uploads/", http.StripPrefix("/uploads/", imgServer))
 | 
			
		||||
 | 
			
		||||
	})
 | 
			
		||||
	r.Group(func(mux chi.Router) {
 | 
			
		||||
		mux.Use(AuthenticationMiddleware)
 | 
			
		||||
		mux.Post("/users/me/avatar", citadelHandler.ProfileImageUpload)
 | 
			
		||||
		mux.Handle("/graphql", graph.NewHandler(*repository))
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	return r, nil
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user