package auth import ( "encoding/json" "fmt" "log" "net/http" "gitea.local/admin/hspguard/internal/repository" "gitea.local/admin/hspguard/internal/types" "gitea.local/admin/hspguard/internal/util" "gitea.local/admin/hspguard/internal/web" "github.com/avct/uasurfer" ) type LoginParams struct { Email string `json:"email"` Password string `json:"password"` } func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) { var params LoginParams decoder := json.NewDecoder(r.Body) if err := decoder.Decode(¶ms); err != nil { web.Error(w, "failed to parse request body", http.StatusBadRequest) return } if params.Email == "" || params.Password == "" { web.Error(w, "missing required fields", http.StatusBadRequest) return } log.Printf("DEBUG: looking for user with following params: %#v\n", params) user, err := h.repo.FindUserEmail(r.Context(), params.Email) if err != nil { web.Error(w, "user with provided email does not exists", http.StatusBadRequest) return } if !util.VerifyPassword(params.Password, user.PasswordHash) { web.Error(w, "username or/and password are incorrect", http.StatusBadRequest) return } access, refresh, err := h.signTokens(&user) if err != nil { web.Error(w, "failed to generate tokens", http.StatusInternalServerError) return } userAgent := r.UserAgent() var deviceInfo types.DeviceInfo parsed := uasurfer.Parse(userAgent) deviceInfo.Browser = parsed.Browser.Name.StringTrimPrefix() deviceInfo.BrowserVersion = fmt.Sprintf("%d.%d.%d", parsed.Browser.Version.Major, parsed.Browser.Version.Minor, parsed.Browser.Version.Patch) deviceInfo.DeviceName = fmt.Sprintf("%s %s", parsed.OS.Platform.StringTrimPrefix(), parsed.OS.Name.StringTrimPrefix()) deviceInfo.DeviceType = parsed.DeviceType.StringTrimPrefix() deviceInfo.OS = parsed.OS.Platform.StringTrimPrefix() deviceInfo.OSVersion = fmt.Sprintf("%d.%d.%d", parsed.OS.Version.Major, parsed.OS.Version.Minor, parsed.OS.Version.Patch) deviceInfo.UserAgent = userAgent if location, err := util.GetLocation(r.RemoteAddr); err != nil { log.Println("WARN: Failed to get location from ip (%s): %v\n", r.RemoteAddr, err) } else { deviceInfo.Location = fmt.Sprintf("%s, %s, %s", location.Country, location.Region, location.City) } serialized, err := json.Marshal(deviceInfo) if err != nil { log.Println("ERR: Failed to serialize device info: %v\n", err) serialized = []byte{'{', '}'} } // Create User Session session, err := h.repo.CreateUserSession(r.Context(), repository.CreateUserSessionParams{ UserID: user.ID, SessionType: "user", ExpiresAt: &refresh.ExpiresAt, LastActive: nil, IpAddress: &r.RemoteAddr, UserAgent: &userAgent, AccessTokenID: &access.ID, RefreshTokenID: &refresh.ID, DeviceInfo: serialized, }) if err != nil { log.Println("ERR: Failedd to create user session after logging in: %v\n", err) } log.Println("INFO: User session created for '%s': %#v\n", user.Email, session) if err := h.repo.UpdateLastLogin(r.Context(), user.ID); err != nil { web.Error(w, "failed to update user's last login", http.StatusInternalServerError) return } encoder := json.NewEncoder(w) type Response struct { AccessToken string `json:"access"` RefreshToken string `json:"refresh"` // fields required for UI in account selector, e.g. email, full name and avatar FullName string `json:"full_name"` Email string `json:"email"` Id string `json:"id"` ProfilePicture *string `json:"profile_picture"` // Avatar } w.Header().Set("Content-Type", "application/json") if err := encoder.Encode(Response{ AccessToken: access.Token, RefreshToken: refresh.Token, FullName: user.FullName, Email: user.Email, Id: user.ID.String(), ProfilePicture: user.ProfilePicture, // Avatar }); err != nil { web.Error(w, "failed to encode response", http.StatusInternalServerError) } }