feature: add first time install process
This commit is contained in:
		@@ -1,11 +1,11 @@
 | 
			
		||||
package route
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"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"
 | 
			
		||||
@@ -18,23 +18,26 @@ 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 NewUserAccount struct {
 | 
			
		||||
	FullName string `json:"fullname"`
 | 
			
		||||
	Username string
 | 
			
		||||
	Password string
 | 
			
		||||
	Initials string
 | 
			
		||||
	Email    string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type InstallRequestData struct {
 | 
			
		||||
	User NewUserAccount
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LoginResponseData struct {
 | 
			
		||||
	AccessToken string `json:"accessToken"`
 | 
			
		||||
	IsInstalled bool   `json:"isInstalled"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type LogoutResponseData struct {
 | 
			
		||||
@@ -51,18 +54,48 @@ type AvatarUploadResponseData struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *CitadelHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
	_, err := h.repo.GetSystemOptionByKey(r.Context(), "is_installed")
 | 
			
		||||
	if err == sql.ErrNoRows {
 | 
			
		||||
		user, err := h.repo.GetUserAccountByUsername(r.Context(), "system")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.InstallOnly)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		w.Header().Set("Content-type", "application/json")
 | 
			
		||||
		json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString, IsInstalled: false})
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		log.WithError(err).Error("get system option")
 | 
			
		||||
		w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c, err := r.Cookie("refreshToken")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if err == http.ErrNoCookie {
 | 
			
		||||
			w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		log.WithError(err).Error("unknown error")
 | 
			
		||||
		w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	refreshTokenID := uuid.MustParse(c.Value)
 | 
			
		||||
	token, err := h.repo.GetRefreshTokenByID(r.Context(), refreshTokenID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if err == sql.ErrNoRows {
 | 
			
		||||
 | 
			
		||||
			log.WithError(err).WithFields(log.Fields{"refreshTokenID": refreshTokenID.String()}).Error("no tokens found")
 | 
			
		||||
			w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		log.WithError(err).Error("token retrieve failure")
 | 
			
		||||
		w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -76,7 +109,7 @@ func (h *CitadelHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Requ
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	accessTokenString, err := auth.NewAccessToken(token.UserID.String())
 | 
			
		||||
	accessTokenString, err := auth.NewAccessToken(token.UserID.String(), auth.Unrestricted)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
@@ -88,7 +121,7 @@ func (h *CitadelHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Requ
 | 
			
		||||
		Expires:  refreshExpiresAt,
 | 
			
		||||
		HttpOnly: true,
 | 
			
		||||
	})
 | 
			
		||||
	json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString})
 | 
			
		||||
	json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString, IsInstalled: true})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *CitadelHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
@@ -142,7 +175,7 @@ func (h *CitadelHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	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())
 | 
			
		||||
	accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		w.WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
@@ -154,7 +187,68 @@ func (h *CitadelHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		Expires:  refreshExpiresAt,
 | 
			
		||||
		HttpOnly: true,
 | 
			
		||||
	})
 | 
			
		||||
	json.NewEncoder(w).Encode(LoginResponseData{accessTokenString})
 | 
			
		||||
	json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *CitadelHandler) InstallHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if restricted, ok := r.Context().Value("restricted_mode").(auth.RestrictedMode); ok {
 | 
			
		||||
		if restricted != auth.InstallOnly {
 | 
			
		||||
			log.Warning("attempted to install without install only restriction")
 | 
			
		||||
			w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err := h.repo.GetSystemOptionByKey(r.Context(), "is_installed")
 | 
			
		||||
	if err != sql.ErrNoRows {
 | 
			
		||||
		log.WithError(err).Error("install handler called even though system is installed")
 | 
			
		||||
		w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var requestData InstallRequestData
 | 
			
		||||
	err = json.NewDecoder(r.Body).Decode(&requestData)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.WithFields(log.Fields{"r": requestData}).Info("install")
 | 
			
		||||
 | 
			
		||||
	createdAt := time.Now().UTC()
 | 
			
		||||
	hashedPwd, err := bcrypt.GenerateFromPassword([]byte(requestData.User.Password), 14)
 | 
			
		||||
	user, err := h.repo.CreateUserAccount(r.Context(), db.CreateUserAccountParams{
 | 
			
		||||
		Username:     requestData.User.Username,
 | 
			
		||||
		Initials:     requestData.User.Initials,
 | 
			
		||||
		Email:        requestData.User.Email,
 | 
			
		||||
		PasswordHash: string(hashedPwd),
 | 
			
		||||
		CreatedAt:    createdAt,
 | 
			
		||||
		RoleCode:     "admin",
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	_, err = h.repo.CreateSystemOption(r.Context(), db.CreateSystemOptionParams{Key: "is_installed", Value: sql.NullString{Valid: true, String: "true"}})
 | 
			
		||||
	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{user.UserID, refreshCreatedAt, refreshExpiresAt})
 | 
			
		||||
 | 
			
		||||
	accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted)
 | 
			
		||||
	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, false})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (rs authResource) Routes(citadelHandler CitadelHandler) chi.Router {
 | 
			
		||||
 
 | 
			
		||||
@@ -40,13 +40,19 @@ func AuthenticationMiddleware(next http.Handler) http.Handler {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		userID, err := uuid.Parse(accessClaims.UserID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error(err)
 | 
			
		||||
			w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
			return
 | 
			
		||||
		var userID uuid.UUID
 | 
			
		||||
		if accessClaims.Restricted == auth.InstallOnly {
 | 
			
		||||
			userID = uuid.New()
 | 
			
		||||
		} else {
 | 
			
		||||
			userID, err = uuid.Parse(accessClaims.UserID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.WithError(err).Error("middleware access token userID parse")
 | 
			
		||||
				w.WriteHeader(http.StatusBadRequest)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		ctx := context.WithValue(r.Context(), "userID", userID)
 | 
			
		||||
		ctx = context.WithValue(ctx, "restricted_mode", accessClaims.Restricted)
 | 
			
		||||
 | 
			
		||||
		next.ServeHTTP(w, r.WithContext(ctx))
 | 
			
		||||
	})
 | 
			
		||||
 
 | 
			
		||||
@@ -104,6 +104,7 @@ func NewRouter(config config.AppConfig, dbConnection *sqlx.DB) (chi.Router, erro
 | 
			
		||||
	r.Group(func(mux chi.Router) {
 | 
			
		||||
		mux.Use(AuthenticationMiddleware)
 | 
			
		||||
		mux.Post("/users/me/avatar", citadelHandler.ProfileImageUpload)
 | 
			
		||||
		mux.Post("/auth/install", citadelHandler.InstallHandler)
 | 
			
		||||
		mux.Handle("/graphql", graph.NewHandler(config, *repository))
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user