package api import ( "fmt" "time" "strings" "net/http" "encoding/json" "github.com/google/uuid" log "github.com/sirupsen/logrus" "github.com/lestrrat-go/jwx/jwt" "reichard.io/imagini/internal/models" ) // https://www.calhoun.io/pitfalls-of-context-values-and-how-to-avoid-or-mitigate-them/ // https://pace.dev/blog/2018/05/09/how-I-write-http-services-after-eight-years.html // https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1#333c // https://www.alexedwards.net/blog/organising-database-access <---- best // - TLDR: Do what you're doing, but use closeures for the handlers func (api *API) loginHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { errorJSON(w, "Method is not supported.", http.StatusMethodNotAllowed) return } // Decode into Struct var creds models.APICredentials err := json.NewDecoder(r.Body).Decode(&creds) if err != nil { errorJSON(w, "Invalid parameters.", http.StatusBadRequest) return } // Validate if creds.User == "" || creds.Password == "" { errorJSON(w, "Invalid parameters.", http.StatusBadRequest) return } // Verify Device Name Exists deviceHeader := r.Header.Get("X-Imagini-DeviceName") if deviceHeader == "" { errorJSON(w, "Missing 'X-Imagini-DeviceName' header.", http.StatusBadRequest) return } // Derive Device Type var deviceType string userAgent := strings.ToLower(r.Header.Get("User-Agent")) if strings.HasPrefix(userAgent, "ios-imagini"){ deviceType = "iOS" } else if strings.HasPrefix(userAgent, "android-imagini"){ deviceType = "Android" } else if strings.HasPrefix(userAgent, "chrome"){ deviceType = "Chrome" } else if strings.HasPrefix(userAgent, "firefox"){ deviceType = "Firefox" } else if strings.HasPrefix(userAgent, "msie"){ deviceType = "Internet Explorer" } else if strings.HasPrefix(userAgent, "edge"){ deviceType = "Edge" } else if strings.HasPrefix(userAgent, "safari"){ deviceType = "Safari" }else { deviceType = "Unknown" } // Do login resp, user := api.Auth.AuthenticateUser(creds) if !resp { errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) return } // Create New Device device, err := api.DB.CreateDevice(models.Device{Name: deviceHeader, Type: deviceType}) // Create Tokens accessToken, err := api.Auth.CreateJWTAccessToken(user, device) refreshToken, err := api.Auth.CreateJWTRefreshToken(user, device) // Set appropriate cookies accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken, HttpOnly: true} refreshCookie := http.Cookie{Name: "RefreshToken", Value: refreshToken, HttpOnly: true} http.SetCookie(w, &accessCookie) http.SetCookie(w, &refreshCookie) // Response success successJSON(w, "Login success.", http.StatusOK) } func (api *API) logoutHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method is not supported.", http.StatusMethodNotAllowed) return } // Do logout // TODO: Clear Session Server Side // Tell Client to Expire Token cookie := &http.Cookie{ Name: "Token", Value: "", Path: "/", Expires: time.Unix(0, 0), HttpOnly: true, } http.SetCookie(w, cookie) } func (api *API) refreshLoginHandler(w http.ResponseWriter, r *http.Request) { refreshCookie, err := r.Cookie("RefreshToken") if err != nil { log.Warn("[middleware] Cookie not found") w.WriteHeader(http.StatusUnauthorized) return } // Validate Refresh Token refreshToken, ok := api.Auth.ValidateJWTRefreshToken(refreshCookie.Value) if !ok { http.SetCookie(w, &http.Cookie{Name: "AccessToken", Expires: time.Unix(0, 0)}) http.SetCookie(w, &http.Cookie{Name: "RefreshToken", Expires: time.Unix(0, 0)}) errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) return } // Acquire User & Device (Trusted) did, ok := refreshToken.Get("did") if !ok { errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) return } uid, ok := refreshToken.Get(jwt.SubjectKey) if !ok { errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) return } deviceID, err := uuid.Parse(fmt.Sprintf("%v", did)) if err != nil { errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) return } userID, err := uuid.Parse(fmt.Sprintf("%v", uid)) if err != nil { errorJSON(w, "Invalid credentials.", http.StatusUnauthorized) return } // Device Skeleton user := models.User{Base: models.Base{UUID: userID}} device := models.Device{Base: models.Base{UUID: deviceID}} // Update token accessToken, err := api.Auth.CreateJWTAccessToken(user, device) accessCookie := http.Cookie{Name: "AccessToken", Value: accessToken} http.SetCookie(w, &accessCookie) // Response success successJSON(w, "Refresh success.", http.StatusOK) }