package main import ( "bytes" "database/sql" "encoding/base64" "encoding/json" "errors" "fmt" _ "github.com/go-sql-driver/mysql" "html/template" "io" "log" "math" "net/http" "net/http/httputil" "net/mail" "net/url" "net/smtp" "os" "os/exec" "regexp" "strconv" "strings" "sync" "time" // pdf "github.com/SebastiaanKlippert/go-wkhtmltopdf" "github.com/brianvoe/gofakeit/v6" "github.com/disintegration/gift" "github.com/dustin/go-humanize" "github.com/golang-jwt/jwt/v4" "github.com/stripe/stripe-go/v76" "github.com/stripe/stripe-go/v76/customer" "github.com/stripe/stripe-go/v76/subscription" // "github.com/stripe/stripe-go/v76/invoice" // "github.com/stripe/stripe-go/v76/paymentintent" "github.com/stripe/stripe-go/v76/webhook" "image" _ "image/jpeg" "image/png" ) type Config struct { DBName string DBUsername string DBPassword string } type Address struct { Id int `json:"id"` Full string `json:"full"` Street string `json:"street"` City string `json:"city"` Region string `json:"region"` Country string `json:"country"` Zip string `json:"zip"` } type Branch struct { Id int `json:"id"` Name string `json:"name"` Type string `json:"type"` Letterhead []byte `json:"letterhead"` Num string `json:"num"` Phone string `json:"phone"` Address Address `json:"address"` } type Subscription struct { Id int `json:"id"` UserId int `json:"userId"` StripeId string `json:"stripeId"` CustomerId string `json:"customerId"` PriceId string `json:"priceId"` Start int `json:"start"` End int `json:"end"` ClientSecret string `json:"clientSecret,omitempty"` PaymentStatus string `json:"paymentStatus"` Status string `json:"status"` } type User struct { Id int `json:"id"` Email string `json:"email"` FirstName string `json:"firstName"` LastName string `json:"lastName"` Phone string `json:"phone"` Address Address `json:"address"` Branch Branch `json:"branch"` License License `json:"license"` Sub Subscription `json:"sub"` Status string `json:"status"` Country string `json:"country"` Title string `json:"title"` Verified bool `json:"verified"` Role string `json:"role"` Password string `json:"password,omitempty"` CustomerId string `json:"customerId"` } type License struct { Id int `json:"id"` UserId int `json:"userId"` Type string `json:"type"` Num string `json:"num"` } type UserClaims struct { Id int `json:"id"` Role string `json:"role"` Exp string `json:"exp"` } type VerificationClaims struct { Id int `json:"id"` Exp string `json:"exp"` } type Page struct { tpl *template.Template Title string Name string } type Borrower struct { Id int `json:"id"` Credit int `json:"credit"` Income int `json:"income"` Num int `json:"num"` } type FeeTemplate struct { Id int `json:"id"` User int `json:"user"` Branch int `json:"branch"` Amount int `json:"amount"` Perc float32 `json:"perc"` Type string `json:"type"` Notes string `json:"notes"` Name string `json:"name"` Category string `json:"category"` Auto bool `json:"auto"` } type Fee struct { Id int `json:"id"` LoanId int `json:"loan_id"` Amount int `json:"amount"` Perc float32 `json:"perc"` Type string `json:"type"` Notes string `json:"notes"` Name string `json:"name"` Category string `json:"category"` } type LoanType struct { Id int `json:"id"` User int `json:"user"` Branch int `json:"branch"` Name string `json:"name"` } type Loan struct { Id int `json:"id"` EstimateId int `json:"estimateId"` Type LoanType `json:"type"` Amount int `json:"amount"` Amortization string `json:"amortization"` Term int `json:"term"` Ltv float32 `json:"ltv"` Dti float32 `json:"dti"` Hoi int `json:"hoi"` Hazard int `json:"hazard"` Tax int `json:"tax"` Interest float32 `json:"interest"` Mi MI `json:"mi"` Fees []Fee `json:"fees"` Credits []Fee // Fees with negative amounts for internal use Name string `json:"title"` Result Result `json:"result"` } type MI struct { Type string `json:"user"` Label string `json:"label"` Lender string `json:"lender"` Rate float32 `json:"rate"` Premium int `json:"premium"` Upfront int `json:"upfront"` Monthly bool `json:"monthly"` FiveYearTotal float32 `json:"fiveYearTotal"` InitialAllInPremium float32 `json:"initialAllInPremium"` InitialAllInRate float32 `json:"initialAllInRate"` InitialAmount float32 `json:"initialAmount"` } type Result struct { Id int `json:"id"` LoanId int `json:"loanId"` LoanPayment int `json:"loanPayment"` TotalMonthly int `json:"totalMonthly"` TotalFees int `json:"totalFees"` TotalCredits int `json:"totalCredits"` CashToClose int `json:"cashToClose"` } type Estimate struct { Id int `json:"id"` User int `json:"user"` Borrower Borrower `json:"borrower"` Transaction string `json:"transaction"` Price int `json:"price"` Property string `json:"property"` Occupancy string `json:"occupancy"` Zip string `json:"zip"` Pud bool `json:"pud"` Loans []Loan `json:"loans"` } type ETemplate struct { Id int `json:"id"` Estimate Estimate `json:"estimate"` UserId int `json:"userId"` BranchId int `json:"branchId"` } type Report struct { Title string Name string Avatar string Letterhead string User User Estimate Estimate } type Password struct { Old string `json:"old"` New string `json:"new"` } type Endpoint func(http.ResponseWriter, *sql.DB, *http.Request) type HookKeys struct { InvoicePaid string InvoiceFailed string SubCreated string SubUpdated string SubDeleted string } var ( regexen = make(map[string]*regexp.Regexp) relock sync.Mutex address = "localhost:8001" mainAddress = "localhost:8002" ) var paths = map[string]string{ "home": "views/home.tpl", "terms": "views/terms.tpl", "app": "views/app.tpl", "comparison": "views/report/comparison.tpl", } var pages = map[string]Page{ "home": cache("home", "Home"), "terms": cache("terms", "Terms and Conditions"), "report": cachePdf("comparison"), "app": cache("app", "App"), } var roles = map[string]int{ "User": 1, "Manager": 2, "Admin": 3, } var statuses = map[string]int{ "Unsubscribed": 1, "Trial": 2, "Free": 3, "Subscriber": 4, "Branch": 5, "Admin": 6, } var propertyTypes = []string{ "Single Detached", "Single Attached", "Condo Lo-rise", "Condo Hi-rise", } var feeTypes = []string{ "Government", "Title", "Required", "Lender", "Other", } var hookKeys = HookKeys{ InvoicePaid: "", InvoiceFailed: "", SubCreated: "", SubUpdated: "", SubDeleted: "", } var standardPriceId = "price_1OZLK9BPMoXn2pf9kuTAf8rs" // Used to validate claim in JWT token body. Checks if user id is greater than // zero and time format is valid func (c UserClaims) Valid() error { if c.Id < 1 { return errors.New("Invalid id") } t, err := time.Parse(time.UnixDate, c.Exp) if err != nil { return err } if t.Before(time.Now()) { return errors.New("Token expired.") } return err } func (c VerificationClaims) Valid() error { if c.Id < 1 { return errors.New("Invalid id") } t, err := time.Parse(time.UnixDate, c.Exp) if err != nil { return err } if t.Before(time.Now()) { return errors.New("Token expired.") } return err } func cache(name string, title string) Page { var p = []string{"views/master.tpl", paths[name]} tpl := template.Must(template.ParseFiles(p...)) return Page{tpl: tpl, Title: title, Name: name} } func cachePdf(name string) Page { // Money is stored in cents, so it must be converted to dollars in reports dollars := func(cents int) string { return humanize.Commaf(float64(cents) / 100) } // For calculating down payments diff := func(a, b int) string { return humanize.Commaf(float64(b-a) / 100) } sortFees := func(ftype string, fees []Fee) []Fee { result := make([]Fee, 0) for i := range fees { if fees[i].Type != ftype { continue } result = append(result, fees[i]) } return result } fm := template.FuncMap{ "dollars": dollars, "diff": diff, "sortFees": sortFees} var p = []string{"views/report/master.tpl", "views/report/header.tpl", "views/report/summary.tpl", "views/report/comparison.tpl"} tpl := template.Must(template.New("master.tpl").Funcs(fm).ParseFiles(p...)) return Page{tpl: tpl, Title: "", Name: name} } func (page Page) Render(w http.ResponseWriter) { err := page.tpl.Execute(w, page) if err != nil { log.Print(err) } } func match(path, pattern string, args *[]string) bool { relock.Lock() defer relock.Unlock() regex := regexen[pattern] if regex == nil { regex = regexp.MustCompile("^" + pattern + "$") regexen[pattern] = regex } matches := regex.FindStringSubmatch(path) if len(matches) <= 0 { return false } *args = matches[1:] return true } func (estimate *Estimate) makeResults() []Result { var results []Result amortize := func(principle float64, rate float64, periods float64) int { exp := math.Pow(1+rate, periods) return int(principle * rate * exp / (exp - 1)) } for i := range estimate.Loans { var loan = &estimate.Loans[i] var result Result = Result{} // Monthly payments use amortized loan payment formula plus monthly MI, // plus all other recurring fees result.LoanPayment = amortize(float64(loan.Amount), float64(loan.Interest/100/12), float64(loan.Term*12)) result.TotalMonthly = result.LoanPayment + loan.Hoi + loan.Tax + loan.Hazard if loan.Mi.Monthly { result.TotalMonthly = result.TotalMonthly + int(loan.Mi.Rate/100/12*float32(loan.Amount)) } else { loan.Mi.Upfront = int(loan.Mi.Rate / 100 * float32(loan.Amount)) } for i := range loan.Fees { if loan.Fees[i].Amount > 0 { result.TotalFees = result.TotalFees + loan.Fees[i].Amount } else { result.TotalCredits = result.TotalCredits + loan.Fees[i].Amount } } result.CashToClose = result.TotalFees + result.TotalCredits + (estimate.Price - loan.Amount) result.LoanId = loan.Id loan.Result = result results = append(results, result) } return results } func summarize(w http.ResponseWriter, db *sql.DB, r *http.Request) { var estimate Estimate err := json.NewDecoder(r.Body).Decode(&estimate) if err != nil { http.Error(w, "Invalid estimate.", 422) return } estimate.makeResults() json.NewEncoder(w).Encode(estimate) } func getLoanType(db *sql.DB, id int) (LoanType, error) { types, err := getLoanTypes(db, id, 0, 0) if err != nil { return LoanType{Id: id}, err } if len(types) == 0 { return LoanType{Id: id}, errors.New("No type with that id") } return types[0], nil } func getLoanTypes(db *sql.DB, id int, user int, branch int) ( []LoanType, error) { var loans []LoanType var query = `SELECT id, coalesce(user_id, 0), coalesce(branch_id, 0), name FROM loan_type WHERE loan_type.id = CASE @e := ? WHEN 0 THEN id ELSE @e END OR loan_type.user_id = CASE @e := ? WHEN 0 THEN id ELSE @e END OR loan_type.branch_id = CASE @e := ? WHEN 0 THEN id ELSE @e END` // Should be changed to specify user rows, err := db.Query(query, id, user, branch) if err != nil { return nil, fmt.Errorf("loan_type error: %v", err) } defer rows.Close() for rows.Next() { var loan LoanType if err := rows.Scan( &loan.Id, &loan.User, &loan.Branch, &loan.Name); err != nil { log.Printf("Error occured fetching loan: %v", err) return nil, fmt.Errorf("Error occured fetching loan: %v", err) } loans = append(loans, loan) } return loans, nil } func getFees(db *sql.DB, loan int) ([]Fee, error) { var fees []Fee query := `SELECT id, loan_id, amount, perc, type, notes, name, category FROM fee WHERE loan_id = ?` rows, err := db.Query(query, loan) if err != nil { return nil, fmt.Errorf("Fee query error %v", err) } defer rows.Close() for rows.Next() { var fee Fee if err := rows.Scan( &fee.Id, &fee.LoanId, &fee.Amount, &fee.Perc, &fee.Type, &fee.Notes, &fee.Name, &fee.Category, ); err != nil { return nil, fmt.Errorf("Fees scanning error: %v", err) } fees = append(fees, fee) } return fees, nil } func fetchFeesTemp(db *sql.DB, user int, branch int) ([]FeeTemplate, error) { var fees []FeeTemplate query := `SELECT id, user_id, COALESCE(branch_id, 0), amount, perc, type, notes, name, category, auto FROM fee_template WHERE user_id = ? OR branch_id = ? ` rows, err := db.Query(query, user, branch) if err != nil { return nil, fmt.Errorf("Fee template query error %v", err) } defer rows.Close() for rows.Next() { var fee FeeTemplate if err := rows.Scan( &fee.Id, &fee.User, &fee.Branch, &fee.Amount, &fee.Perc, &fee.Type, &fee.Notes, &fee.Name, &fee.Category, &fee.Auto); err != nil { return nil, fmt.Errorf("FeesTemplate scanning error: %v", err) } fees = append(fees, fee) } return fees, nil } func constructEvent(r *http.Request, key string) (*stripe.Event, error) { b, err := io.ReadAll(r.Body) if err != nil { log.Printf("io.ReadAll: %v", err) return nil, err } event, err := webhook.ConstructEvent(b, r.Header.Get("Stripe-Signature"), key) if err != nil { log.Printf("webhook.ConstructEvent: %v", err) return nil, err } return &event, nil } // Fetch fees from the database func getFeesTemp(w http.ResponseWriter, db *sql.DB, r *http.Request) { var fees []FeeTemplate claims, err := getClaims(r) if err != nil { w.WriteHeader(500) return } user, err := queryUser(db, claims.Id) if err != nil { w.WriteHeader(422) return } fees, err = fetchFeesTemp(db, claims.Id, user.Branch.Id) json.NewEncoder(w).Encode(fees) } // Fetch fees from the database func createFeesTemp(w http.ResponseWriter, db *sql.DB, r *http.Request) { var fee FeeTemplate var query string var row *sql.Row var err error claims, err := getClaims(r) // var id int // Inserted estimate's id err = json.NewDecoder(r.Body).Decode(&fee) if err != nil { w.WriteHeader(422) return } query = `INSERT INTO fee_template ( user_id, branch_id, amount, perc, type, notes, name, auto ) VALUES (?, NULL, ?, ?, ?, ?, ?, ?) RETURNING id ` row = db.QueryRow(query, claims.Id, fee.Amount, fee.Perc, fee.Type, fee.Notes, fee.Name, fee.Auto, ) err = row.Scan(&fee.Id) if err != nil { w.WriteHeader(500) return } json.NewEncoder(w).Encode(fee) } // Fetch fees from the database func deleteFeeTemp(w http.ResponseWriter, db *sql.DB, r *http.Request) { var fee FeeTemplate var query string var err error // claims, err := getClaims(r) // var id int // Inserted estimate's id err = json.NewDecoder(r.Body).Decode(&fee) if err != nil { w.WriteHeader(422) return } query = `DELETE FROM fee_template WHERE id = ?` _, err = db.Exec(query, fee.Id) if err != nil { w.WriteHeader(500) return } } func deleteEstimate(w http.ResponseWriter, db *sql.DB, r *http.Request) { var estimate Estimate var err error err = json.NewDecoder(r.Body).Decode(&estimate) if err != nil { w.WriteHeader(422) return } claims, err := getClaims(r) err = estimate.del(db, claims.Id) if err != nil { http.Error(w, err.Error(), 500) return } } func deleteET(w http.ResponseWriter, db *sql.DB, r *http.Request) { var et ETemplate var err error err = json.NewDecoder(r.Body).Decode(&et) if err != nil { w.WriteHeader(422) return } claims, err := getClaims(r) err = et.del(db, claims.Id) if err != nil { http.Error(w, err.Error(), 500) return } } func getMi(db *sql.DB, loan int) (MI, error) { var mi MI query := `SELECT type, label, lender, rate, premium, upfront, five_year_total, initial_premium, initial_rate, initial_amount FROM mi WHERE loan_id = ?` rows, err := db.Query(query, loan) if err != nil { return mi, err } defer rows.Close() if !rows.Next() { return mi, nil } if err := rows.Scan( &mi.Type, &mi.Label, &mi.Lender, &mi.Rate, &mi.Premium, &mi.Upfront, &mi.FiveYearTotal, &mi.InitialAllInPremium, &mi.InitialAllInRate, &mi.InitialAmount, ); err != nil { return mi, err } return mi, nil } func (estimate *Estimate) getBorrower(db *sql.DB) error { query := `SELECT id, credit_score, monthly_income, num FROM borrower WHERE estimate_id = ? LIMIT 1` row := db.QueryRow(query, estimate.Id) if err := row.Scan( &estimate.Borrower.Id, &estimate.Borrower.Credit, &estimate.Borrower.Income, &estimate.Borrower.Num, ); err != nil { return fmt.Errorf("Borrower scanning error: %v", err) } return nil } // Query Lender APIs and parse responses into MI structs func fetchMi(db *sql.DB, estimate *Estimate, pos int) []MI { var loan Loan = estimate.Loans[pos] var ltv = func(l float32) string { switch { case l > 95: return "LTV97" case l > 90: return "LTV95" case l > 85: return "LTV90" default: return "LTV85" } } var term = func(t int) string { switch { case t <= 10: return "A10" case t <= 15: return "A15" case t <= 20: return "A20" case t <= 25: return "A25" case t <= 30: return "A30" default: return "A40" } } var propertyCodes = map[string]string{ "Single Attached": "SFO", "Single Detached": "SFO", "Condo Lo-rise": "CON", "Condo Hi-rise": "CON", } var purposeCodes = map[string]string{ "Purchase": "PUR", "Refinance": "RRT", } body, err := json.Marshal(map[string]any{ "zipCode": estimate.Zip, "stateCode": "CA", "address": "", "propertyTypeCode": propertyCodes[estimate.Property], "occupancyTypeCode": "PRS", "loanPurposeCode": purposeCodes[estimate.Transaction], "loanAmount": loan.Amount, "loanToValue": ltv(loan.Ltv), "amortizationTerm": term(loan.Term), "loanTypeCode": "FXD", "duLpDecisionCode": "DAE", "loanProgramCodes": []any{}, "debtToIncome": loan.Dti, "wholesaleLoan": 0, "coveragePercentageCode": "L30", "productCode": "BPM", "renewalTypeCode": "CON", "numberOfBorrowers": 1, "coBorrowerCreditScores": []any{}, "borrowerCreditScore": strconv.Itoa(estimate.Borrower.Credit), "masterPolicy": nil, "selfEmployedIndicator": false, "armType": "", "userId": 44504, }) /* if err != nil { log.Printf("Could not marshal NationalMI body: \n%v\n%v\n", bytes.NewBuffer(body), err) func queryAddress(db *sql.DB, id int) ( Address, error ) { var address Address = Address{Id: id} var err error row := db.QueryRow( `SELECT id, full_address, street, city, region, country, zip FROM address WHERE id = ?`, id) err = row.Scan( &address.Id, &address.Full, &address.Street, &address.City, &address.Region, &address.Country, &address.Zip, ) return address, err } } */ req, err := http.NewRequest("POST", "https://rate-gps.nationalmi.com/rates/productRateQuote", bytes.NewBuffer(body)) req.Header.Add("Content-Type", "application/json") req.AddCookie(&http.Cookie{ Name: "nmirategps_email", Value: os.Getenv("NationalMIEmail")}) resp, err := http.DefaultClient.Do(req) var res map[string]interface{} var result []MI if resp.StatusCode != 200 { log.Printf("the status: %v\nthe resp: %v\n the req: %v\n the body: %v\n", resp.Status, resp, req.Body, bytes.NewBuffer(body)) } else { json.NewDecoder(resp.Body).Decode(&res) // estimate.Loans[pos].Mi = res // Parse res into result here } log.Println(err) return result } // Make comparison PDF func generatePDF(w http.ResponseWriter, db *sql.DB, r *http.Request) { } func login(w http.ResponseWriter, db *sql.DB, r *http.Request) { var id int var role string var err error var user User json.NewDecoder(r.Body).Decode(&user) row := db.QueryRow( `SELECT id, role FROM user WHERE email = ? AND password = sha2(?, 256)`, user.Email, user.Password, ) err = row.Scan(&id, &role) if err != nil { http.Error(w, "Invalid Credentials.", http.StatusUnauthorized) return } err = setTokenCookie(id, role, w) if err != nil { http.Error(w, err.Error(), 500) } } func setTokenCookie(id int, role string, w http.ResponseWriter) error { token := jwt.NewWithClaims(jwt.SigningMethodHS256, UserClaims{Id: id, Role: role, Exp: time.Now().Add(time.Minute * 30).Format(time.UnixDate)}) tokenStr, err := token.SignedString([]byte(os.Getenv("JWT_SECRET"))) if err != nil { log.Println("Token could not be signed: ", err, tokenStr) return err } cookie := http.Cookie{Name: "skouter", Value: tokenStr, Path: "/", Expires: time.Now().Add(time.Hour * 1)} http.SetCookie(w, &cookie) return nil } func refreshToken(w http.ResponseWriter, db *sql.DB, r *http.Request) { claims, _ := getClaims(r) if !claims.Valid() { return } err := setTokenCookie(claims.Id, claims.Role, w) if err != nil { http.Error(w, "Token generation error", http.StatusInternalServerError) } return } func getClaims(r *http.Request) (UserClaims, error) { claims := new(UserClaims) _, tokenStr, found := strings.Cut(r.Header.Get("Authorization"), " ") if !found { return *claims, errors.New("Token not found") } // Pull token payload into UserClaims _, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (any, error) { return []byte(os.Getenv("JWT_SECRET")), nil }) if err != nil { return *claims, err } if err = claims.Valid(); err != nil { return *claims, err } return *claims, nil } func guard(r *http.Request, required int) bool { claims, err := getClaims(r) if err != nil { return false } if roles[claims.Role] < required { return false } return true } // Inserts an address and returns it's ID along with any errors. func insertAddress(db *sql.DB, address Address) (int, error) { var query string var row *sql.Row var err error var id int // Inserted user's id query = `INSERT INTO address ( full_address, street, city, region, country, zip ) VALUES (?, ?, ?, ?, ?, ?) RETURNING id ` row = db.QueryRow(query, address.Full, address.Street, address.City, address.Region, address.Country, address.Zip, ) err = row.Scan(&id) return id, err } // Inserts an address and returns it's ID along with any errors. func insertBranch(db *sql.DB, branch Branch) (int, error) { var query string var row *sql.Row var err error var id int // Inserted user's id query = `INSERT INTO branch ( name, type, letterhead, num, phone, address ) VALUES (?, ?, ?, ?, ?, ?) RETURNING id ` row = db.QueryRow(query, branch.Name, branch.Type, branch.Letterhead, branch.Num, branch.Phone, branch.Address.Id, ) err = row.Scan(&id) return id, err } // Inserts an address and returns it's ID along with any errors. func insertLicense(db *sql.DB, license License) (int, error) { var query string var row *sql.Row var err error var id int // Inserted license's id query = `INSERT INTO license ( user_id, type, num ) VALUES (?, ?, ?) RETURNING id ` row = db.QueryRow(query, license.UserId, license.Type, license.Num, ) err = row.Scan(&id) return id, err } func queryLicense(db *sql.DB, user int) (License, error) { var license License = License{UserId: user} var err error row := db.QueryRow( `SELECT id, type, num FROM license WHERE user_id = ?`, user) err = row.Scan( &license.Id, &license.Type, &license.Num, ) return license, err } func queryAddress(db *sql.DB, id int) (Address, error) { var address Address = Address{Id: id} var err error row := db.QueryRow( `SELECT id, full_address, street, city, region, country, zip FROM address WHERE id = ?`, id) err = row.Scan( &address.Id, &address.Full, &address.Street, &address.City, &address.Region, &address.Country, &address.Zip, ) return address, err } func queryBranch(db *sql.DB, id int) (Branch, error) { var branch Branch = Branch{Id: id} var err error row := db.QueryRow( `SELECT id, name, type, letterhead, num, phone, address FROM branch WHERE id = ?`, id) err = row.Scan( &branch.Id, &branch.Name, &branch.Type, &branch.Letterhead, &branch.Num, &branch.Phone, &branch.Address.Id, ) return branch, err } func queryUser(db *sql.DB, id int) (User, error) { var user User var query string var err error query = `SELECT u.id, u.email, u.first_name, u.last_name, coalesce(u.branch_id, 0), u.country, u.title, coalesce(u.status, ''), coalesce(u.customer_id, ''), u.verified, u.role, u.address, u.phone FROM user u WHERE u.id = ? ` row := db.QueryRow(query, id) if err != nil { return user, err } err = row.Scan( &user.Id, &user.Email, &user.FirstName, &user.LastName, &user.Branch.Id, &user.Country, &user.Title, &user.Status, &user.CustomerId, &user.Verified, &user.Role, &user.Address.Id, &user.Phone, ) if err != nil { return user, err } user.Address, err = queryAddress(db, user.Address.Id) if err != nil { return user, err } if user.Branch.Id > 0 { user.Branch, err = queryBranch(db, user.Branch.Id) if err != nil { return user, err } } return user, nil } func queryCustomer(db *sql.DB, id string) (User, error) { var user User var query string var err error query = `SELECT u.id, u.email, u.first_name, u.last_name, coalesce(u.branch_id, 0), u.country, u.title, coalesce(u.status, ''), coalesce(u.customer_id, ''), u.verified, u.role, u.address, u.phone FROM user u WHERE u.customer_id = ? ` row := db.QueryRow(query, id) if err != nil { return user, err } err = row.Scan( &user.Id, &user.Email, &user.FirstName, &user.LastName, &user.Branch.Id, &user.Country, &user.Title, &user.Status, &user.CustomerId, &user.Verified, &user.Role, &user.Address.Id, &user.Phone, ) if err != nil { return user, err } user.Address, err = queryAddress(db, user.Address.Id) if err != nil { return user, err } user.Branch, err = queryBranch(db, user.Branch.Id) if err != nil { return user, err } return user, nil } // Can probably be deleted. func queryUsers(db *sql.DB, id int) ([]User, error) { var users []User var query string var rows *sql.Rows var err error query = `SELECT u.id, u.email, u.first_name, u.last_name, coalesce(u.branch_id, 0), u.country, u.title, coalesce(u.status, ''), u.verified, u.role, u.address, u.phone FROM user u WHERE u.id = CASE @e := ? WHEN 0 THEN u.id ELSE @e END ` rows, err = db.Query(query, id) if err != nil { return users, err } defer rows.Close() for rows.Next() { var user User if err := rows.Scan( &user.Id, &user.Email, &user.FirstName, &user.LastName, &user.Branch.Id, &user.Country, &user.Title, &user.Status, &user.Verified, &user.Role, &user.Address.Id, &user.Phone, ); err != nil { return users, err } user.Address, err = queryAddress(db, user.Address.Id) if err != nil { return users, err } user.Branch, err = queryBranch(db, user.Branch.Id) if err != nil { return users, err } users = append(users, user) } // Prevents runtime panics if len(users) == 0 { return users, errors.New("User not found.") } return users, nil } func querySub(db *sql.DB, id int) (Subscription, error) { var query string var err error var s Subscription query = `SELECT id, stripe_id, user_id, customer_id, current_period_end, current_period_start, client_secret, payment_status FROM subscription WHERE id = ? ` row := db.QueryRow(query, id) err = row.Scan( &s.Id, &s.StripeId, &s.CustomerId, &s.End, &s.Start, &s.ClientSecret, &s.PaymentStatus, ) return s, err } func (user *User) querySub(db *sql.DB) error { var query string var err error query = `SELECT id, stripe_id, user_id, customer_id, price_id, current_period_end, current_period_start, client_secret, payment_status FROM subscription WHERE user_id = ? ` row := db.QueryRow(query, user.Id) err = row.Scan( &user.Sub.Id, &user.Sub.StripeId, &user.Sub.UserId, &user.Sub.CustomerId, &user.Sub.PriceId, &user.Sub.End, &user.Sub.Start, &user.Sub.ClientSecret, &user.Sub.PaymentStatus, ) return err } func (estimate *Estimate) insertResults(db *sql.DB) error { var query string var row *sql.Row var err error var id int query = `INSERT INTO estimate_result ( loan_id, loan_payment, total_monthly, total_fees, total_credits, cash_to_close ) VALUES (?, ?, ?, ?, ?, ?) RETURNING id ` for i := range estimate.Loans { r := estimate.Loans[i].Result r.LoanId = estimate.Loans[i].Id row = db.QueryRow(query, r.LoanId, r.LoanPayment, r.TotalMonthly, r.TotalFees, r.TotalCredits, r.CashToClose, ) err = row.Scan(&id) if err != nil { return err } r.Id = id } return nil } // Insert user returning it's ID or any error func insertUser(db *sql.DB, user User) (int, error) { var query string var row *sql.Row var err error var id int // Inserted user's id user.Address.Id, err = insertAddress(db, user.Address) if err != nil { return 0, err } query = `INSERT INTO user ( email, first_name, last_name, password, role, title, status, verified, address, country, branch_id, phone, created, last_login ) VALUES (?, ?, ?, sha2(?, 256), ?, ?, ?, ?, ?, ?, CASE @b := ? WHEN 0 THEN NULL ELSE @b END, ?, NOW(), NOW()) RETURNING id ` row = db.QueryRow(query, user.Email, user.FirstName, user.LastName, user.Password, user.Role, user.Title, user.Status, user.Verified, user.Address.Id, user.Country, user.Branch.Id, user.Phone, ) err = row.Scan(&id) if err != nil { return 0, err } user.Id = id return id, nil } // Insert user returning it's ID or any error func (sub *Subscription) insertSub(db *sql.DB) (error) { var query string var row *sql.Row var err error query = `INSERT INTO subscription ( stripe_id, user_id, customer_id, price_id, current_period_end, current_period_start, client_secret, payment_status, status ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id ` row = db.QueryRow(query, sub.StripeId, sub.UserId, sub.CustomerId, sub.PriceId, sub.End, sub.Start, sub.ClientSecret, sub.PaymentStatus, sub.Status, ) err = row.Scan(&sub.Id) return err } func (sub *Subscription) updateSub(db *sql.DB) error { var query string var err error query = `UPDATE subscription SET client_secret = CASE @a := ? WHEN '' THEN client_secret ELSE @a END, current_period_end = CASE @b := ? WHEN 0 THEN current_period_end ELSE @b END, current_period_start = CASE @c := ? WHEN 0 THEN current_period_start ELSE @c END, payment_status = CASE @d := ? WHEN '' THEN client_secret ELSE @d END, status = CASE @e := ? WHEN '' THEN client_secret ELSE @e END WHERE id = ? ` _, err = db.Exec(query, sub.ClientSecret, sub.End, sub.Start, sub.PaymentStatus, sub.Status, sub.Id, ) if err != nil { return err } return err } // Updates a user's stripe customer ID. func (user *User) updateCustomerId(db *sql.DB, cid string) (error) { var query string var err error query = `UPDATE user SET customer_id = ? WHERE id = ? ` _, err = db.Exec(query, cid, user.Id, ) if err != nil { return err } user.CustomerId = cid return nil } func updateAddress(address Address, db *sql.DB) error { query := ` UPDATE address SET full_address = CASE @e := ? WHEN '' THEN full_address ELSE @e END, street = CASE @fn := ? WHEN '' THEN street ELSE @fn END, city = CASE @ln := ? WHEN '' THEN city ELSE @ln END, region = CASE @r := ? WHEN '' THEN region ELSE @r END, country = CASE @r := ? WHEN '' THEN country ELSE @r END, zip = CASE @r := ? WHEN '' THEN zip ELSE @r END WHERE id = ? ` _, err := db.Exec(query, address.Full, address.Street, address.City, address.Region, address.Country, address.Zip, address.Id, ) return err } func (user *User) update(db *sql.DB) error { query := ` UPDATE user SET email = CASE @e := ? WHEN '' THEN email ELSE @e END, first_name = CASE @fn := ? WHEN '' THEN first_name ELSE @fn END, last_name = CASE @ln := ? WHEN '' THEN last_name ELSE @ln END, role = CASE @r := ? WHEN '' THEN role ELSE @r END, password = CASE @p := ? WHEN '' THEN password ELSE sha2(@p, 256) END WHERE id = ? ` _, err := db.Exec(query, user.Email, user.FirstName, user.LastName, user.Role, user.Password, user.Id, ) return err } func getUser(w http.ResponseWriter, db *sql.DB, r *http.Request) { claims, err := getClaims(r) if err != nil { w.WriteHeader(500) return } user, err := queryUser(db, claims.Id) if err != nil { w.WriteHeader(422) log.Println(err) return } json.NewEncoder(w).Encode(user) } func getUsers(w http.ResponseWriter, db *sql.DB, r *http.Request) { users, err := queryUsers(db, 0) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } json.NewEncoder(w).Encode(users) } // Updates a user using only specified values in the JSON body func setUser(user User, db *sql.DB) error { _, err := mail.ParseAddress(user.Email) if err != nil { return err } if roles[user.Role] == 0 { return errors.New("Invalid role") } err = user.update(db) if err != nil { return err } err = updateAddress(user.Address, db) if err != nil { return err } return nil } func patchUser(w http.ResponseWriter, db *sql.DB, r *http.Request) { var user User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { http.Error(w, "Invalid fields", 422) return } err = setUser(user, db) if err != nil { http.Error(w, err.Error(), 422) return } } // Update specified fields of the user specified in the claim func patchSelf(w http.ResponseWriter, db *sql.DB, r *http.Request) { claim, err := getClaims(r) var user User json.NewDecoder(r.Body).Decode(&user) // First check that the target user to be updated is the same as the claim id, and // their role is unchanged. if err != nil || claim.Id != user.Id { http.Error(w, "Target user's id does not match claim.", 401) return } if claim.Role != user.Role && user.Role != "" { http.Error(w, "Administrator required to escalate role.", 401) return } err = setUser(user, db) if err != nil { http.Error(w, err.Error(), 422) return } } func deleteUser(w http.ResponseWriter, db *sql.DB, r *http.Request) { var user User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { http.Error(w, "Invalid fields.", 422) return } query := `DELETE FROM user WHERE id = ?` _, err = db.Exec(query, user.Id) if err != nil { http.Error(w, "Could not delete.", 500) } } // Checks if a user's entries are reasonable before database insertion. // This function is very important because it is the only thing preventing // anyone from creating an admin user. These error messages are displayed to // the user. func (user *User) validate() error { _, err := mail.ParseAddress(user.Email) if err != nil { return errors.New("Invalid email.") } if roles[user.Role] == 0 { return errors.New("Invalid role.") } if roles[user.Role] == roles["Admin"] { return errors.New("New user cannot be an Admin.") } if user.FirstName == "" { return errors.New("Given name cannot be empty.") } if user.LastName == "" { return errors.New("Surname cannot be empty.") } if user.Password == "" { return errors.New("Empty password") } return nil } func createUser(w http.ResponseWriter, db *sql.DB, r *http.Request) { var user User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { http.Error(w, "Invalid fields.", 422) return } user.Role = "User" user.Status = "Trial" err = user.validate() if err != nil { http.Error(w, err.Error(), 422) return } user.Id, err = insertUser(db, user) if err != nil { http.Error(w, err.Error(), 422) return } err = setTokenCookie(user.Id, user.Role, w) if err != nil { http.Error(w, err.Error(), 500) return } json.NewEncoder(w).Encode(user) user.sendVerificationEmail() } func checkPassword(db *sql.DB, id int, pass string) bool { var p string query := `SELECT password FROM user WHERE user.id = ? AND password = sha2(?, 256) ` row := db.QueryRow(query, id, pass) err := row.Scan(&p) if err != nil { return false } return true } func setPassword(db *sql.DB, id int, pass string) error { query := `UPDATE user SET password = sha2(?, 256) WHERE user.id = ? ` _, err := db.Exec(query, pass, id) if err != nil { return errors.New("Could not insert password.") } return nil } func changePassword(w http.ResponseWriter, db *sql.DB, r *http.Request) { var pass Password claim, err := getClaims(r) err = json.NewDecoder(r.Body).Decode(&pass) if err != nil { http.Error(w, "Bad fields.", 422) return } if checkPassword(db, claim.Id, pass.Old) { err = setPassword(db, claim.Id, pass.New) } else { http.Error(w, "Incorrect old password.", 401) return } if err != nil { http.Error(w, err.Error(), 500) return } } func fetchAvatar(db *sql.DB, user int) ([]byte, error) { var img []byte var query string var err error query = `SELECT avatar FROM user WHERE user.id = ? ` row := db.QueryRow(query, user) err = row.Scan(&img) if err != nil { return img, err } return img, nil } func insertAvatar(db *sql.DB, user int, img []byte) error { query := `UPDATE user SET avatar = ? WHERE id = ? ` _, err := db.Exec(query, img, user) if err != nil { return err } return nil } func fetchLetterhead(db *sql.DB, user int) ([]byte, error) { var img []byte var query string var err error query = `SELECT letterhead FROM user WHERE user.id = ? ` row := db.QueryRow(query, user) err = row.Scan(&img) if err != nil { return img, err } return img, nil } func insertLetterhead(db *sql.DB, user int, img []byte) error { query := `UPDATE user SET letterhead = ? WHERE id = ? ` _, err := db.Exec(query, img, user) if err != nil { return err } return nil } func setAvatar(w http.ResponseWriter, db *sql.DB, r *http.Request) { var validTypes []string = []string{"image/png", "image/jpeg"} var isValidType bool claims, err := getClaims(r) if err != nil { http.Error(w, "Invalid token.", 422) return } img, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Invalid file.", 422) return } for _, v := range validTypes { if v == http.DetectContentType(img) { isValidType = true } } if !isValidType { http.Error(w, "Invalid file type.", 422) return } err = insertAvatar(db, claims.Id, img) if err != nil { http.Error(w, "Could not insert.", 500) return } } func getAvatar(w http.ResponseWriter, db *sql.DB, r *http.Request) { claims, err := getClaims(r) if err != nil { http.Error(w, "Invalid token.", 422) return } img, err := fetchAvatar(db, claims.Id) if err != nil { http.Error(w, "Could not retrieve.", 500) return } w.Header().Set("Content-Type", http.DetectContentType(img)) w.Write(img) } func setLetterhead(w http.ResponseWriter, db *sql.DB, r *http.Request) { var validTypes []string = []string{"image/png", "image/jpeg"} var isValidType bool claims, err := getClaims(r) if err != nil { http.Error(w, "Invalid token.", 422) return } img, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Invalid file.", 422) return } for _, v := range validTypes { if v == http.DetectContentType(img) { isValidType = true } } if !isValidType { http.Error(w, "Invalid file type.", 422) return } err = insertLetterhead(db, claims.Id, img) if err != nil { http.Error(w, "Could not insert.", 500) return } } func getLetterhead(w http.ResponseWriter, db *sql.DB, r *http.Request) { claims, err := getClaims(r) if err != nil { http.Error(w, "Invalid token.", 422) return } img, err := fetchLetterhead(db, claims.Id) if err != nil { http.Error(w, "Could not retrieve.", 500) return } w.Header().Set("Content-Type", http.DetectContentType(img)) w.Write(img) } func queryBorrower(db *sql.DB, id int) (Borrower, error) { var borrower Borrower var query string var err error query = `SELECT l.id, l.credit_score, l.num, l.monthly_income FROM borrower l WHERE l.id = ? ` row := db.QueryRow(query, id) err = row.Scan( borrower.Id, borrower.Credit, borrower.Num, borrower.Income, ) if err != nil { return borrower, err } return borrower, nil } // Must have an estimate ID 'e', but not necessarily a loan id 'id' func getResults(db *sql.DB, e int, id int) ([]Result, error) { var results []Result var query string var rows *sql.Rows var err error query = `SELECT r.id, loan_id, loan_payment, total_monthly, total_fees, total_credits, cash_to_close FROM estimate_result r INNER JOIN loan ON r.loan_id = loan.id WHERE r.id = CASE @e := ? WHEN 0 THEN r.id ELSE @e END AND loan.estimate_id = ? ` rows, err = db.Query(query, id, e) if err != nil { return results, err } defer rows.Close() for rows.Next() { var result Result if err := rows.Scan( &result.Id, &result.LoanId, &result.LoanPayment, &result.TotalMonthly, &result.TotalFees, &result.TotalCredits, &result.CashToClose, ); err != nil { return results, err } results = append(results, result) } // Prevents runtime panics // if len(results) == 0 { return results, errors.New("Result not found.") } return results, nil } // Retrieve an estimate result with a specified loan id func getResult(db *sql.DB, loan int) (Result, error) { var result Result var query string var err error query = `SELECT r.id, loan_id, loan_payment, total_monthly, total_fees, total_credits, cash_to_close FROM estimate_result r INNER JOIN loan ON r.loan_id = loan.id WHERE loan.Id = ? ` row := db.QueryRow(query, loan) err = row.Scan( &result.Id, &result.LoanId, &result.LoanPayment, &result.TotalMonthly, &result.TotalFees, &result.TotalCredits, &result.CashToClose, ) if err != nil { return result, err } return result, nil } // Must have an estimate ID 'e', but not necessarily a loan id 'id' func getLoans(db *sql.DB, e int, id int) ([]Loan, error) { var loans []Loan var query string var rows *sql.Rows var err error query = `SELECT l.id, l.type_id, l.estimate_id, l.amount, l.term, l.interest, l.ltv, l.dti, l.hoi, l.tax, l.name FROM loan l WHERE l.id = CASE @e := ? WHEN 0 THEN l.id ELSE @e END AND l.estimate_id = ? ` rows, err = db.Query(query, id, e) if err != nil { return loans, err } defer rows.Close() for rows.Next() { var loan Loan if err := rows.Scan( &loan.Id, &loan.Type.Id, &loan.EstimateId, &loan.Amount, &loan.Term, &loan.Interest, &loan.Ltv, &loan.Dti, &loan.Hoi, &loan.Tax, &loan.Name, ); err != nil { return loans, err } mi, err := getMi(db, loan.Id) if err != nil { return loans, err } loan.Mi = mi fees, err := getFees(db, loan.Id) if err != nil { return loans, err } loan.Fees = fees loan.Result, err = getResult(db, loan.Id) if err != nil { return loans, err } loan.Type, err = getLoanType(db, loan.Type.Id) if err != nil { return loans, err } loans = append(loans, loan) } // Prevents runtime panics if len(loans) == 0 { return loans, errors.New("Loan not found.") } return loans, nil } func getEstimate(db *sql.DB, id int) (Estimate, error) { estimates, err := getEstimates(db, id, 0) if err != nil { return Estimate{}, err } return estimates[0], nil } func getEstimates(db *sql.DB, id int, user int) ([]Estimate, error) { var estimates []Estimate var query string var rows *sql.Rows var err error query = `SELECT id, user_id, transaction, price, property, occupancy, zip, pud FROM estimate WHERE id = CASE @e := ? WHEN 0 THEN id ELSE @e END AND user_id = CASE @e := ? WHEN 0 THEN user_id ELSE @e END ` rows, err = db.Query(query, id, user) if err != nil { return estimates, err } defer rows.Close() for rows.Next() { var estimate Estimate if err := rows.Scan( &estimate.Id, &estimate.User, &estimate.Transaction, &estimate.Price, &estimate.Property, &estimate.Occupancy, &estimate.Zip, &estimate.Pud, ); err != nil { return estimates, err } err := estimate.getBorrower(db) if err != nil { return estimates, err } estimates = append(estimates, estimate) } // Prevents runtime panics if len(estimates) == 0 { return estimates, errors.New("Estimate not found.") } for i := range estimates { estimates[i].Loans, err = getLoans(db, estimates[i].Id, 0) if err != nil { return estimates, err } } return estimates, nil } func queryETemplates(db *sql.DB, id int, user int) ([]ETemplate, error) { var eTemplates []ETemplate var query string var rows *sql.Rows var err error query = `SELECT id, estimate_id, user_id, branch_id FROM estimate_template WHERE id = CASE @e := ? WHEN 0 THEN id ELSE @e END AND user_id = CASE @e := ? WHEN 0 THEN user_id ELSE @e END ` rows, err = db.Query(query, id, user) if err != nil { return eTemplates, err } defer rows.Close() for rows.Next() { var e ETemplate if err := rows.Scan( &e.Id, &e.Estimate.Id, &e.UserId, &e.BranchId, ); err != nil { return eTemplates, err } e.Estimate, err = getEstimate(db, e.Estimate.Id) if err != nil { return eTemplates, err } eTemplates = append(eTemplates, e) } return eTemplates, nil } // Accepts a borrower struct and returns the id of the inserted borrower and // any related error. func (estimate *Estimate) insertETemplate(db *sql.DB, user int, branch int) error { var query string var err error query = `INSERT INTO estimate_template ( estimate_id, user_id, branch_id ) VALUES (?, ?, ?) ` _, err = db.Exec(query, estimate.Id, user, branch, ) if err != nil { return err } return nil } // Accepts a borrower struct and returns the id of the inserted borrower and // any related error. func (estimate *Estimate) insertBorrower(db *sql.DB) error { var query string var row *sql.Row var err error query = `INSERT INTO borrower ( estimate_id, credit_score, monthly_income, num ) VALUES (?, ?, ?, ?) RETURNING id ` row = db.QueryRow(query, estimate.Id, estimate.Borrower.Credit, estimate.Borrower.Income, estimate.Borrower.Num, ) err = row.Scan(&estimate.Borrower.Id) if err != nil { return err } return nil } func insertMi(db *sql.DB, mi MI) (int, error) { var id int query := `INSERT INTO mi ( type, label, lender, rate, premium, upfront, five_year_total, initial_premium, initial_rate, initial_amount ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id` row := db.QueryRow(query, &mi.Type, &mi.Label, &mi.Lender, &mi.Rate, &mi.Premium, &mi.Upfront, &mi.FiveYearTotal, &mi.InitialAllInPremium, &mi.InitialAllInRate, &mi.InitialAmount, ) err := row.Scan(&id) if err != nil { return 0, err } return id, nil } func insertFee(db *sql.DB, fee Fee) (int, error) { var id int query := `INSERT INTO fee (loan_id, amount, perc, type, notes, name, category) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id` row := db.QueryRow(query, fee.LoanId, fee.Amount, fee.Perc, fee.Type, fee.Notes, fee.Name, fee.Category, ) err := row.Scan(&id) if err != nil { return 0, err } return id, nil } func insertLoanType(db *sql.DB, lt LoanType) (int, error) { var id int query := `INSERT INTO loan_type (branch_id, user_id, name) VALUES (NULLIF(?, 0), NULLIF(?, 0), ?) RETURNING id` row := db.QueryRow(query, lt.Branch, lt.User, lt.Name, ) err := row.Scan(&id) if err != nil { return 0, err } return id, nil } func (loan *Loan) insertLoan(db *sql.DB) error { var query string var row *sql.Row var err error query = `INSERT INTO loan ( estimate_id, type_id, amount, term, interest, ltv, dti, hoi, tax, name ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id ` row = db.QueryRow(query, loan.EstimateId, loan.Type.Id, loan.Amount, loan.Term, loan.Interest, loan.Ltv, loan.Dti, loan.Hoi, loan.Tax, loan.Name, ) err = row.Scan(&loan.Id) if err != nil { return err } _, err = insertMi(db, loan.Mi) if err != nil { return err } for i := range loan.Fees { loan.Fees[i].LoanId = loan.Id _, err := insertFee(db, loan.Fees[i]) if err != nil { return err } } return nil } func (estimate *Estimate) insertEstimate(db *sql.DB) error { var query string var row *sql.Row var err error // var id int // Inserted estimate's id query = `INSERT INTO estimate ( user_id, transaction, price, property, occupancy, zip, pud ) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING id ` row = db.QueryRow(query, estimate.User, estimate.Transaction, estimate.Price, estimate.Property, estimate.Occupancy, estimate.Zip, estimate.Pud, ) err = row.Scan(&estimate.Id) if err != nil { return err } err = estimate.insertBorrower(db) if err != nil { return err } for i := range estimate.Loans { estimate.Loans[i].EstimateId = estimate.Id err = estimate.Loans[i].insertLoan(db) if err != nil { return err } } return nil } func (estimate *Estimate) del(db *sql.DB, user int) error { var query string var err error query = `DELETE FROM estimate WHERE id = ? AND user_id = ?` _, err = db.Exec(query, estimate.Id, user) if err != nil { return err } return nil } func (et *ETemplate) del(db *sql.DB, user int) error { var query string var err error query = `DELETE FROM estimate_template WHERE id = ? AND user_id = ?` _, err = db.Exec(query, et.Id, user) if err != nil { return err } return nil } func (eTemplate *ETemplate) insert(db *sql.DB) error { var query string var row *sql.Row var err error query = `INSERT INTO estimate_template ( user_id, branch_id, estimate_id, ) VALUES (?, ?, ?) RETURNING id ` row = db.QueryRow(query, eTemplate.UserId, eTemplate.BranchId, eTemplate.Estimate.Id, ) err = row.Scan(&eTemplate.Id) if err != nil { return err } return nil } func createEstimate(w http.ResponseWriter, db *sql.DB, r *http.Request) { var estimate Estimate err := json.NewDecoder(r.Body).Decode(&estimate) if err != nil { http.Error(w, "Invalid fields.", 422) return } claims, err := getClaims(r) estimate.User = claims.Id err = estimate.insertEstimate(db) if err != nil { http.Error(w, err.Error(), 422) return } estimate.makeResults() err = estimate.insertResults(db) if err != nil { http.Error(w, err.Error(), 500) return } e, err := getEstimates(db, estimate.Id, 0) if err != nil { http.Error(w, err.Error(), 500) return } json.NewEncoder(w).Encode(e[0]) } // Query all estimates that belong to the current user func fetchEstimate(w http.ResponseWriter, db *sql.DB, r *http.Request) { var estimates []Estimate claims, err := getClaims(r) estimates, err = getEstimates(db, 0, claims.Id) if err != nil { http.Error(w, err.Error(), 500) return } json.NewEncoder(w).Encode(estimates) } func checkConventional(l Loan, b Borrower) error { if b.Credit < 620 { return errors.New("Credit score too low for conventional loan") } // Buyer needs to put down 20% to avoid mortgage insurance if l.Ltv > 80 && l.Mi.Rate == 0 { return errors.New(fmt.Sprintln( l.Name, "down payment must be 20% to avoid insurance", )) } return nil } func checkFHA(l Loan, b Borrower) error { if b.Credit < 500 { return errors.New("Credit score too low for FHA loan") } if l.Ltv > 96.5 { return errors.New("FHA down payment must be at least 3.5%") } if b.Credit < 580 && l.Ltv > 90 { return errors.New("FHA down payment must be at least 10%") } // Debt-to-income must be below 45% if credit score is below 580. if b.Credit < 580 && l.Dti > 45 { return errors.New(fmt.Sprintln( l.Name, "debt to income is too high for credit score.", )) } return nil } // Loan option for veterans with no set rules. Mainly placeholder. func checkVA(l Loan, b Borrower) error { return nil } // Loan option for residents of rural areas with no set rules. // Mainly placeholder. func checkUSDA(l Loan, b Borrower) error { return nil } // Should also check loan amount limit maybe with an API. func checkEstimate(e Estimate) error { if e.Property == "" { return errors.New("Empty property type") } if e.Price == 0 { return errors.New("Empty property price") } if e.Borrower.Num == 0 { return errors.New("Missing number of borrowers") } if e.Borrower.Credit == 0 { return errors.New("Missing borrower credit score") } if e.Borrower.Income == 0 { return errors.New("Missing borrower credit income") } for _, l := range e.Loans { if l.Amount == 0 { return errors.New(fmt.Sprintln(l.Name, "loan amount cannot be zero")) } if l.Term == 0 { return errors.New(fmt.Sprintln(l.Name, "loan term cannot be zero")) } if l.Interest == 0 { return errors.New(fmt.Sprintln(l.Name, "loan interest cannot be zero")) } // Can be used to check rules for specific loan types var err error switch l.Type.Id { case 1: err = checkConventional(l, e.Borrower) case 2: err = checkFHA(l, e.Borrower) case 3: err = checkVA(l, e.Borrower) case 4: err = checkUSDA(l, e.Borrower) default: err = errors.New("Invalid loan type") } if err != nil { return err } } return nil } func validateEstimate(w http.ResponseWriter, db *sql.DB, r *http.Request) { var estimate Estimate err := json.NewDecoder(r.Body).Decode(&estimate) if err != nil { http.Error(w, err.Error(), 422) return } err = checkEstimate(estimate) if err != nil { http.Error(w, err.Error(), 406) return } } func checkPdf(w http.ResponseWriter, r *http.Request) { db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/skouter_dev", os.Getenv("DBUser"), os.Getenv("DBPass"))) // w.Header().Set("Content-Type", "application/json; charset=UTF-8") err = db.Ping() if err != nil { fmt.Println("Bad database configuration: %v\n", err) panic(err) // maybe os.Exit(1) instead } estimates, err := getEstimates(db, 1, 0) if err != nil { w.WriteHeader(500) return } // claims, err := getClaims(r) if err != nil { w.WriteHeader(500) return } user, err := queryUser(db, 1) info := Report{ Title: "test PDF", Name: "idk-random-name", User: user, Estimate: estimates[0], } avatar, err := fetchAvatar(db, info.User.Id) letterhead, err := fetchLetterhead(db, info.User.Id) info.Avatar = base64.StdEncoding.EncodeToString(avatar) info.Letterhead = base64.StdEncoding.EncodeToString(letterhead) for l := range info.Estimate.Loans { loan := info.Estimate.Loans[l] for f := range info.Estimate.Loans[l].Fees { if info.Estimate.Loans[l].Fees[f].Amount < 0 { loan.Credits = append(loan.Credits, loan.Fees[f]) } } } err = pages["report"].tpl.ExecuteTemplate(w, "master.tpl", info) if err != nil { fmt.Println(err) } } func getETemplates(w http.ResponseWriter, db *sql.DB, r *http.Request) { claims, err := getClaims(r) et, err := queryETemplates(db, 0, claims.Id) if err != nil { http.Error(w, err.Error(), 500) return } json.NewEncoder(w).Encode(et) } func createETemplate(w http.ResponseWriter, db *sql.DB, r *http.Request) { var estimate Estimate err := json.NewDecoder(r.Body).Decode(&estimate) if err != nil { http.Error(w, err.Error(), 422) return } claims, err := getClaims(r) if err != nil { http.Error(w, err.Error(), 422) return } err = estimate.insertETemplate(db, claims.Id, 0) if err != nil { http.Error(w, err.Error(), 500) return } } func getPdf(w http.ResponseWriter, db *sql.DB, r *http.Request) { var estimate Estimate err := json.NewDecoder(r.Body).Decode(&estimate) cmd := exec.Command("wkhtmltopdf", "-", "-") stdout, err := cmd.StdoutPipe() if err != nil { w.WriteHeader(500) log.Println(err) return } stdin, err := cmd.StdinPipe() if err != nil { w.WriteHeader(500) log.Println(err) return } if err := cmd.Start(); err != nil { log.Fatal(err) } claims, err := getClaims(r) if err != nil { w.WriteHeader(500) log.Println(err) return } user, err := queryUser(db, claims.Id) info := Report{ Title: "test PDF", Name: "idk-random-name", User: user, Estimate: estimate, } avatar, err := fetchAvatar(db, info.User.Id) letterhead, err := fetchLetterhead(db, info.User.Id) if len(avatar) > 1 { info.Avatar = base64.StdEncoding.EncodeToString(avatar) } if len(letterhead) > 1 { info.Letterhead = base64.StdEncoding.EncodeToString(letterhead) } err = pages["report"].tpl.ExecuteTemplate(stdin, "master.tpl", info) if err != nil { w.WriteHeader(500) log.Println(err) return } stdin.Close() buf, err := io.ReadAll(stdout) if _, err := w.Write(buf); err != nil { w.WriteHeader(500) log.Println(err) return } if err := cmd.Wait(); err != nil { log.Println(err) return } } func clipLetterhead(w http.ResponseWriter, db *sql.DB, r *http.Request) { var validTypes []string = []string{"image/png", "image/jpeg"} var isValidType bool var err error // claims, err := getClaims(r) if err != nil { http.Error(w, "Invalid token.", 422) return } img, t, err := image.Decode(r.Body) if err != nil { http.Error(w, "Invalid file, JPEG and PNG only.", 422) return } for _, v := range validTypes { if v == "image/"+t { isValidType = true } } if !isValidType { http.Error(w, "Invalid file type.", 422) return } g := gift.New( gift.ResizeToFit(400, 200, gift.LanczosResampling), ) dst := image.NewRGBA(g.Bounds(img.Bounds())) g.Draw(dst, img) w.Header().Set("Content-Type", "image/png") err = png.Encode(w, dst) if err != nil { http.Error(w, "Error encoding.", 500) return } } func createCustomer(name string, email string, address Address) ( stripe.Customer, error) { params := &stripe.CustomerParams{ Email: stripe.String(email), Name: stripe.String(name), Address: &stripe.AddressParams{ City: stripe.String(address.City), Country: stripe.String(address.Country), Line1: stripe.String(address.Street), PostalCode: stripe.String(address.Zip), State: stripe.String(address.Region), }, }; result, err := customer.New(params) return *result, err } // Initiates a new standard subscription using a given customer ID func createSubscription(cid string) (*stripe.Subscription, error) { // Automatically save the payment method to the subscription // when the first payment is successful. paymentSettings := &stripe.SubscriptionPaymentSettingsParams{ SaveDefaultPaymentMethod: stripe.String("on_subscription"), } // Create the subscription. Note we're expanding the Subscription's // latest invoice and that invoice's payment_intent // so we can pass it to the front end to confirm the payment subscriptionParams := &stripe.SubscriptionParams{ Customer: stripe.String(cid), Items: []*stripe.SubscriptionItemsParams{ { Price: stripe.String(standardPriceId), }, }, PaymentSettings: paymentSettings, PaymentBehavior: stripe.String("default_incomplete"), } subscriptionParams.AddExpand("latest_invoice.payment_intent") s, err := subscription.New(subscriptionParams) return s, err } // Initiates a new trial subscription using a given customer ID func createTrialSubscription(cid string) (*stripe.Subscription, error) { // Automatically save the payment method to the subscription // when the first payment is successful. paymentSettings := &stripe.SubscriptionPaymentSettingsParams{ SaveDefaultPaymentMethod: stripe.String("on_subscription"), } // Create the subscription. Note we're expanding the Subscription's // latest invoice and that invoice's payment_intent // so we can pass it to the front end to confirm the payment subscriptionParams := &stripe.SubscriptionParams{ Customer: stripe.String(cid), Items: []*stripe.SubscriptionItemsParams{ { Price: stripe.String(standardPriceId), }, }, TrialPeriodDays: stripe.Int64(30), PaymentSettings: paymentSettings, PaymentBehavior: stripe.String("default_incomplete"), } subscriptionParams.AddExpand("latest_invoice.payment_intent") s, err := subscription.New(subscriptionParams) return s, err } func ( user *User ) SyncSub( sub *stripe.Subscription, db *sql.DB ) error { var err error user.Sub.UserId = user.Id user.Sub.StripeId = sub.ID user.Sub.CustomerId = user.CustomerId user.Sub.PriceId = standardPriceId user.Sub.End = int(sub.CurrentPeriodEnd) user.Sub.Start = int(sub.CurrentPeriodStart) user.Sub.ClientSecret = sub.LatestInvoice.PaymentIntent.ClientSecret user.Sub.PaymentStatus = string(sub.LatestInvoice.PaymentIntent.Status) user.Sub.Status = string(sub.Status) if user.Sub.Id != 0 { err = user.Sub.insertSub(db) } else { user.Sub.updateSub(db) } return err } // Creates a new subscription instance for a new user or retrieves the // existing instance if possible. It's main purpose is to supply a // client secret used for sending billing information to stripe. func subscribe(w http.ResponseWriter, db *sql.DB, r *http.Request) { claims, err := getClaims(r) user, err := queryUser(db, claims.Id) if err != nil { w.WriteHeader(422) return } user.querySub(db) var name string = user.FirstName + " " + user.LastName if user.CustomerId == "" { c, err := createCustomer(name, user.Email, user.Address) if err != nil { http.Error(w, err.Error(), 500) return } err = user.updateCustomerId(db, c.ID) if err != nil { http.Error(w, err.Error(), 500) return } } if user.Sub.Id == 0 { s, err := createSubscription(user.CustomerId) if err != nil { http.Error(w, err.Error(), 500) return } user.Sub.UserId = user.Id user.Sub.StripeId = s.ID user.Sub.CustomerId = user.CustomerId user.Sub.PriceId = standardPriceId user.Sub.End = int(s.CurrentPeriodEnd) user.Sub.Start = int(s.CurrentPeriodStart) user.Sub.ClientSecret = s.LatestInvoice.PaymentIntent.ClientSecret user.Sub.PaymentStatus = string(s.LatestInvoice.PaymentIntent.Status) // Inserting from here is unnecessary and confusing because // new subs are already handled by the stripe hook. It remains for // easier testing of the endpoint. err = user.Sub.insertSub(db) if err != nil { http.Error(w, err.Error(), 500) return } } else { // This should handle creating a new subscription when the old one // has an incomplete_expired status and cannot be paid } json.NewEncoder(w).Encode(user.Sub) } // Creates a new subscription instance for a new user or retrieves the // existing instance if possible. It's main purpose is to supply a // client secret used for sending billing information to stripe. func trialSubscribe(w http.ResponseWriter, db *sql.DB, r *http.Request) { claims, err := getClaims(r) user, err := queryUser(db, claims.Id) if err != nil { w.WriteHeader(422) return } user.querySub(db) var name string = user.FirstName + " " + user.LastName if user.CustomerId == "" { c, err := createCustomer(name, user.Email, user.Address) if err != nil { http.Error(w, err.Error(), 500) return } err = user.updateCustomerId(db, c.ID) if err != nil { http.Error(w, err.Error(), 500) return } } if user.Sub.Id == 0 { s, err := createTrialSubscription(user.CustomerId) if err != nil { http.Error(w, err.Error(), 500) return } user.Sub.UserId = user.Id user.Sub.StripeId = s.ID user.Sub.CustomerId = user.CustomerId user.Sub.PriceId = standardPriceId user.Sub.End = int(s.CurrentPeriodEnd) user.Sub.Start = int(s.CurrentPeriodStart) user.Sub.ClientSecret = s.LatestInvoice.PaymentIntent.ClientSecret user.Sub.PaymentStatus = string(s.LatestInvoice.PaymentIntent.Status) // Inserting from here is unnecessary and confusing because // new subs are already handled by the stripe hook. It remains for // easier testing of the endpoint. err = user.Sub.insertSub(db) if err != nil { http.Error(w, err.Error(), 500) return } } else { // This should handle creating a new subscription when the old one // has an incomplete_expired status and cannot be paid } json.NewEncoder(w).Encode(user.Sub) } // A successful subscription payment should be confirmed by Stripe and // Updated through this hook. func invoicePaid(w http.ResponseWriter, db *sql.DB, r *http.Request) { var invoice stripe.Invoice b, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) log.Printf("io.ReadAll: %v", err) return } event, err := webhook.ConstructEvent(b, r.Header.Get("Stripe-Signature"), hookKeys.InvoicePaid) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) log.Printf("webhook.ConstructEvent: %v", err) return } // OK should be sent before any processing to confirm with Stripe that // the hook was received w.WriteHeader(http.StatusOK) if event.Type != "invoice.paid" { log.Println("Invalid event type sent to invoice-paid.") return } json.Unmarshal(event.Data.Raw, &invoice) log.Println(event.Type, invoice.ID, invoice.Customer.ID) user, err := queryCustomer(db, invoice.Customer.ID) if err != nil { log.Printf("Could not query customer: %v", err) return } s, err := subscription.Get(invoice.Subscription.ID, nil) if err != nil { log.Printf("Could not fetch subscription: %v", err) return } log.Println(user.Id, s.ID) } func invoiceFailed(w http.ResponseWriter, db *sql.DB, r *http.Request) { b, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) log.Printf("io.ReadAll: %v", err) return } event, err := webhook.ConstructEvent(b, r.Header.Get("Stripe-Signature"), os.Getenv("STRIPE_SECRET_KEY")) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) log.Printf("webhook.ConstructEvent: %v", err) return } log.Println(event.Data) } // Important for catching subscription creation through Stripe dashboard // although it already happens at subscribe(). func subCreated(w http.ResponseWriter, db *sql.DB, r *http.Request) { var sub stripe.Subscription var err error event, err := constructEvent(r, hookKeys.SubCreated) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // OK should be sent before any processing to confirm with Stripe that // the hook was received w.WriteHeader(http.StatusOK) if event.Type != "customer.subscription.created" { log.Println( "Invalid event type. Expecting customer.subscription.created.") return } json.Unmarshal(event.Data.Raw, &sub) log.Println(event.Type, sub.ID, sub.Customer.ID) user, err := queryCustomer(db, sub.Customer.ID) if err != nil { log.Printf("Could not query customer: %v", err) return } if statuses[user.Status] < 5 && sub.Status == "trialing" { user.Status = "Trial" user.update(db) } else if sub.Status != "active" { user.Status = "Unsubscribed" user.update(db) } err = user.SyncSub(&sub, db) if err != nil { http.Error(w, err.Error(), 500) return } log.Println("User subscription created:", user.Id, sub.ID) } // Checks if the user already has a subscription and replaces those // fields if necessary. func subUpdated(w http.ResponseWriter, db *sql.DB, r *http.Request) { var sub stripe.Subscription var err error event, err := constructEvent(r, hookKeys.SubUpdated) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // OK should be sent before any processing to confirm with Stripe that // the hook was received w.WriteHeader(http.StatusOK) if event.Type != "customer.subscription.updated" { log.Println( "Invalid event type sent. Expecting customer.subscription.updated.") return } json.Unmarshal(event.Data.Raw, &sub) log.Println(event.Type, sub.ID, sub.Customer.ID) user, err := queryCustomer(db, sub.Customer.ID) if err != nil { log.Printf("Could not query customer: %v", err) return } if statuses[user.Status] < 5 && sub.Status == "trialing" { user.Status = "Trial" user.update(db) } else if sub.Status != "active" { user.Status = "Unsubscribed" user.update(db) } err = user.SyncSub(&sub, db) if err != nil { http.Error(w, err.Error(), 500) return } log.Println("User subscription created:", user.Id, sub.ID) } // Handles deleted subscriptions hooks sent by Stripe func subDeleted(w http.ResponseWriter, db *sql.DB, r *http.Request) { var sub stripe.Subscription var err error event, err := constructEvent(r, hookKeys.SubDeleted) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // OK should be sent before any processing to confirm with Stripe that // the hook was received w.WriteHeader(http.StatusOK) if event.Type != "customer.subscription.deleted" { log.Println( "Invalid event type sent. Expecting customer.subscription.deleted.") return } json.Unmarshal(event.Data.Raw, &sub) log.Println(event.Type, sub.ID, sub.Customer.ID) user, err := queryCustomer(db, sub.Customer.ID) if err != nil { log.Printf("Could not query customer: %v", err) return } if statuses[user.Status] < 5 && sub.Status == "trialing" { user.Status = "Trial" user.update(db) } else if sub.Status != "active" { user.Status = "Unsubscribed" user.update(db) } user.Sub.Status = "canceled" err = user.SyncSub(&sub, db) if err != nil { http.Error(w, err.Error(), 500) return } log.Println("User subscription created:", user.Id, sub.ID) } func verificationToken(id int) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, VerificationClaims{Id: id, Exp: time.Now().Add(time.Hour * 48).Format(time.UnixDate)}) tokenStr, err := token.SignedString([]byte(os.Getenv("JWT_SECRET"))) if err != nil { log.Println("Verification could not be signed: ", err) return tokenStr, err } return tokenStr, nil } func verifyUser(w http.ResponseWriter, db *sql.DB, r *http.Request) { var claims VerificationClaims params, err := url.ParseQuery(r.URL.Path) if err != nil { w.WriteHeader(500) log.Println(err) return } tokenStr := params.Get("verification_token") // Pull token payload into UserClaims _, err = jwt.ParseWithClaims(tokenStr, &claims, func(token *jwt.Token) (any, error) { return []byte(os.Getenv("JWT_SECRET")), nil }) if err != nil { w.WriteHeader(500) log.Println("Could not parse verification claim.") return } if err = claims.Valid(); err != nil { w.WriteHeader(500) log.Println("Verification claim invalid. ID:", claims.Id) return } } func (user *User) sendVerificationEmail() { auth := smtp.PlainAuth("", os.Getenv("SMTP_USERNAME"), os.Getenv("SMTP_PASSWORD"), os.Getenv("SMTP_HOST")) address := os.Getenv("SMTP_HOST") + ":" + os.Getenv("SMTP_PORT") message := `Subject: Email Verification Welcome %s, Click the link below to verify your email address https://skouter.net?verification_token=%s` t, err := verificationToken(user.Id) if err != nil { return } message = fmt.Sprintf(message, user.FirstName, t) err = smtp.SendMail(address, auth, os.Getenv("SMTP_EMAIL"), []string{user.Email}, []byte(message)) if err != nil { fmt.Println(err) return } log.Println("Email Sent Successfully!") } func api(w http.ResponseWriter, r *http.Request) { var args []string p := r.URL.Path db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s", os.Getenv("DBUser"), os.Getenv("DBPass"), os.Getenv("DBName"), )) w.Header().Set("Content-Type", "application/json; charset=UTF-8") err = db.Ping() if err != nil { fmt.Println("Bad database configuration: %v\n", err) panic(err) // maybe os.Exit(1) instead } refreshToken(w, db, r) switch { case match(p, "/api/refreshTokeen", &args): // Dummy case to trigger refreshToken() without sending 404 case match(p, "/api/login", &args) && r.Method == http.MethodPost: login(w, db, r) case match(p, "/api/token", &args) && r.Method == http.MethodGet && guard(r, 1): getToken(w, db, r) case match(p, "/api/letterhead", &args) && r.Method == http.MethodPost && guard(r, 1): clipLetterhead(w, db, r) case match(p, "/api/users", &args) && // Array of all users r.Method == http.MethodGet && guard(r, 3): getUsers(w, db, r) case match(p, "/api/user", &args) && r.Method == http.MethodGet && guard(r, 1): getUser(w, db, r) case match(p, "/api/user", &args) && r.Method == http.MethodPost: createUser(w, db, r) case match(p, "/api/user", &args) && r.Method == http.MethodPatch && guard(r, 3): // For admin to modify any user patchUser(w, db, r) case match(p, "/api/user", &args) && r.Method == http.MethodPatch && guard(r, 1): // For employees to modify own accounts patchSelf(w, db, r) case match(p, "/api/user", &args) && r.Method == http.MethodDelete && guard(r, 3): deleteUser(w, db, r) case match(p, "/api/user/verify", &args) && r.Method == http.MethodGet: verifyUser(w, db, r) case match(p, "/api/user/avatar", &args) && r.Method == http.MethodGet && guard(r, 1): getAvatar(w, db, r) case match(p, "/api/user/avatar", &args) && r.Method == http.MethodPost && guard(r, 1): setAvatar(w, db, r) case match(p, "/api/user/letterhead", &args) && r.Method == http.MethodGet && guard(r, 1): getLetterhead(w, db, r) case match(p, "/api/user/letterhead", &args) && r.Method == http.MethodPost && guard(r, 1): setLetterhead(w, db, r) case match(p, "/api/user/password", &args) && r.Method == http.MethodPost && guard(r, 1): changePassword(w, db, r) case match(p, "/api/user/subscribe", &args) && r.Method == http.MethodPost && guard(r, 1): subscribe(w, db, r) case match(p, "/api/user/trial", &args) && r.Method == http.MethodPost && guard(r, 1): trialSubscribe(w, db, r) case match(p, "/api/fees", &args) && r.Method == http.MethodGet && guard(r, 1): getFeesTemp(w, db, r) case match(p, "/api/fee", &args) && r.Method == http.MethodPost && guard(r, 1): createFeesTemp(w, db, r) case match(p, "/api/fee", &args) && r.Method == http.MethodDelete && guard(r, 1): deleteFeeTemp(w, db, r) case match(p, "/api/estimates", &args) && r.Method == http.MethodGet && guard(r, 1): fetchEstimate(w, db, r) case match(p, "/api/estimate", &args) && r.Method == http.MethodPost && guard(r, 1): createEstimate(w, db, r) case match(p, "/api/estimate", &args) && r.Method == http.MethodDelete && guard(r, 1): deleteEstimate(w, db, r) case match(p, "/api/estimate/validate", &args) && r.Method == http.MethodPost && guard(r, 1): validateEstimate(w, db, r) case match(p, "/api/estimate/summarize", &args) && r.Method == http.MethodPost && guard(r, 1): summarize(w, db, r) case match(p, "/api/templates", &args) && r.Method == http.MethodGet && guard(r, 1): getETemplates(w, db, r) case match(p, "/api/templates", &args) && r.Method == http.MethodPost && guard(r, 1): createETemplate(w, db, r) case match(p, "/api/templates", &args) && r.Method == http.MethodDelete && guard(r, 1): deleteET(w, db, r) case match(p, "/api/pdf", &args) && r.Method == http.MethodPost && guard(r, 1): getPdf(w, db, r) case match(p, "/api/stripe/invoice-paid", &args) && r.Method == http.MethodPost: invoicePaid(w, db, r) case match(p, "/api/stripe/invoice-payment-failed", &args) && r.Method == http.MethodPost: invoiceFailed(w, db, r) case match(p, "/api/stripe/sub-created", &args) && r.Method == http.MethodPost: subCreated(w, db, r) case match(p, "/api/stripe/sub-updated", &args) && r.Method == http.MethodPost: subUpdated(w, db, r) case match(p, "/api/stripe/sub-deleted", &args) && r.Method == http.MethodPost: subDeleted(w, db, r) default: http.Error(w, "Invalid route or token", 404) } db.Close() } func route(w http.ResponseWriter, r *http.Request) { var page Page var args []string p := r.URL.Path switch { case r.Method == "GET" && match(p, "/", &args): page = pages["home"] case match(p, "/terms", &args): page = pages["terms"] case match(p, "/app", &args): page = pages["app"] default: http.NotFound(w, r) return } page.Render(w) } func serve() { files := http.FileServer(http.Dir("")) proxy, err := url.Parse("http://localhost:8002") if err != nil { log.Fatal("invalid origin server URL") } http.Handle("/assets/", files) http.HandleFunc("/api/", api) http.HandleFunc("/app", route) http.Handle("/", httputil.NewSingleHostReverseProxy(proxy)) log.Fatal(http.ListenAndServe(address, nil)) } func dbReset(db *sql.DB) { b, err := os.ReadFile("migrations/reset.sql") if err != nil { log.Fatal(err) } _, err = db.Exec(string(b)) if err != nil { log.Fatal(err) } b, err = os.ReadFile("migrations/0_29092022_setup_tables.sql") if err != nil { log.Fatal(err) } _, err = db.Exec(string(b)) if err != nil { log.Fatal(err) } } func generateFees(loan Loan) []Fee { var fees []Fee var fee Fee p := gofakeit.Float32Range(0.5, 10) size := gofakeit.Number(1, 10) for f := 0; f < size; f++ { fee = Fee{ Amount: int(float32(loan.Amount) * p / 100), Perc: p, Name: gofakeit.BuzzWord(), Type: feeTypes[gofakeit.Number(0, len(feeTypes)-1)], } fees = append(fees, fee) } return fees } func generateCredits(loan Loan) []Fee { var fees []Fee var fee Fee p := gofakeit.Float32Range(-10, -0.5) size := gofakeit.Number(1, 10) for f := 0; f < size; f++ { fee = Fee{ Amount: int(float32(loan.Amount) * p / 100), Perc: p, Name: gofakeit.BuzzWord(), Type: feeTypes[gofakeit.Number(0, len(feeTypes)-1)], } fees = append(fees, fee) } return fees } func seedAddresses(db *sql.DB) []Address { addresses := make([]Address, 10) for i, a := range addresses { a.Street = gofakeit.Street() a.City = gofakeit.City() a.Region = gofakeit.State() a.Country = "Canada" a.Full = fmt.Sprintf("%s, %s %s", a.Street, a.City, a.Region) id, err := insertAddress(db, a) if err != nil { log.Println(err) break } addresses[i].Id = id } return addresses } func seedBranches(db *sql.DB, addresses []Address) []Branch { branches := make([]Branch, 4) for i := range branches { branches[i].Name = gofakeit.Company() branches[i].Type = "NMLS" branches[i].Letterhead = gofakeit.ImagePng(400, 200) branches[i].Num = gofakeit.HexUint8() branches[i].Phone = gofakeit.Phone() branches[i].Address.Id = gofakeit.Number(1, 5) id, err := insertBranch(db, branches[i]) if err != nil { log.Println(err) break } branches[i].Id = id } return branches } func seedUsers(db *sql.DB, addresses []Address, branches []Branch) []User { users := make([]User, 10) for i := range users { p := gofakeit.Person() users[i].FirstName = p.FirstName users[i].LastName = p.LastName users[i].Email = p.Contact.Email users[i].Phone = p.Contact.Phone users[i].Branch = branches[gofakeit.Number(0, 3)] users[i].Address = addresses[gofakeit.Number(1, 9)] // users[i].Letterhead = gofakeit.ImagePng(400, 200) // users[i].Avatar = gofakeit.ImagePng(200, 200) users[i].Country = []string{"Canada", "USA"}[gofakeit.Number(0, 1)] users[i].Password = "test123" users[i].Verified = true users[i].Title = "Loan Officer" users[i].Status = "Subscriber" users[i].Role = "User" } users[0].Email = "test@example.com" users[0].Email = "test@example.com" users[1].Email = "test2@example.com" users[1].Status = "Branch" users[1].Role = "Manager" users[2].Email = "test3@example.com" users[2].Status = "Free" users[2].Role = "Admin" for i := range users { var err error users[i].Id, err = insertUser(db, users[i]) if err != nil { log.Println(err) break } } return users } func seedLicenses(db *sql.DB, users []User) []License { licenses := make([]License, len(users)) for i := range licenses { licenses[i].UserId = users[i].Id licenses[i].Type = []string{"NMLS", "FSRA"}[gofakeit.Number(0, 1)] licenses[i].Num = gofakeit.UUID() id, err := insertLicense(db, licenses[i]) if err != nil { log.Println(err) break } licenses[i].Id = id } return licenses } func seedLoanTypes(db *sql.DB) []LoanType { var loantypes []LoanType var loantype LoanType var err error loantype = LoanType{Branch: 0, User: 0, Name: "Conventional"} loantype.Id, err = insertLoanType(db, loantype) if err != nil { panic(err) } loantypes = append(loantypes, loantype) loantype = LoanType{Branch: 0, User: 0, Name: "FHA"} loantype.Id, err = insertLoanType(db, loantype) if err != nil { panic(err) } loantypes = append(loantypes, loantype) loantype = LoanType{Branch: 0, User: 0, Name: "USDA"} loantype.Id, err = insertLoanType(db, loantype) if err != nil { panic(err) } loantypes = append(loantypes, loantype) loantype = LoanType{Branch: 0, User: 0, Name: "VA"} loantype.Id, err = insertLoanType(db, loantype) if err != nil { panic(err) } loantypes = append(loantypes, loantype) return loantypes } func seedEstimates(db *sql.DB, users []User, ltypes []LoanType) []Estimate { var estimates []Estimate var estimate Estimate var l Loan var err error for i := 0; i < 15; i++ { estimate = Estimate{} estimate.User = users[gofakeit.Number(0, len(users)-1)].Id estimate.Borrower = Borrower{ Credit: gofakeit.Number(600, 800), Income: gofakeit.Number(1000000, 15000000), Num: gofakeit.Number(1, 20), } estimate.Transaction = []string{"Purchase", "Refinance"}[gofakeit.Number(0, 1)] estimate.Price = gofakeit.Number(50000, 200000000) estimate.Property = propertyTypes[gofakeit.Number(0, len(propertyTypes)-1)] estimate.Occupancy = []string{"Primary", "Secondary", "Investment"}[gofakeit.Number(0, 2)] estimate.Zip = gofakeit.Zip() lsize := gofakeit.Number(1, 6) for j := 0; j < lsize; j++ { l.Type = ltypes[gofakeit.Number(0, len(ltypes)-1)] l.Amount = gofakeit.Number( int(float32(estimate.Price)*0.5), int(float32(estimate.Price)*0.93)) l.Term = gofakeit.Number(4, 30) l.Hoi = gofakeit.Number(50000, 700000) l.Hazard = gofakeit.Number(5000, 200000) l.Tax = gofakeit.Number(5000, 200000) l.Interest = gofakeit.Float32Range(0.5, 8) l.Fees = generateFees(l) l.Credits = generateCredits(l) l.Name = gofakeit.AdjectiveDescriptive() estimate.Loans = append(estimate.Loans, l) } estimates = append(estimates, estimate) } estimates[0].User = users[0].Id estimates[1].User = users[0].Id for i := range estimates { err = estimates[i].insertEstimate(db) if err != nil { log.Println(err) return estimates } } return estimates } func seedResults(db *sql.DB, estimates []Estimate) error { var err error for i := range estimates { estimates[i].makeResults() err = estimates[i].insertResults(db) if err != nil { log.Println(err) return err } } return nil } func dbSeed(db *sql.DB) { addresses := seedAddresses(db) branches := seedBranches(db, addresses) users := seedUsers(db, addresses, branches) _ = seedLicenses(db, users) loantypes := seedLoanTypes(db) estimates := seedEstimates(db, users, loantypes) _ = seedResults(db, estimates) } func dev(args []string) { os.Setenv("DBName", "skouter_dev") os.Setenv("DBUser", "tester") os.Setenv("DBPass", "test123") stripe.Key = os.Getenv("STRIPE_SECRET_KEY") standardPriceId = "price_1OZLK9BPMoXn2pf9kuTAf8rs" hookKeys = HookKeys{ InvoicePaid: os.Getenv("DEV_WEBHOOK_KEY"), InvoiceFailed: os.Getenv("DEV_WEBHOOK_KEY"), SubCreated: os.Getenv("DEV_WEBHOOK_KEY"), SubUpdated: os.Getenv("DEV_WEBHOOK_KEY"), SubDeleted: os.Getenv("DEV_WEBHOOK_KEY"), } db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s?multiStatements=true", os.Getenv("DBUser"), os.Getenv("DBPass"), os.Getenv("DBName"), )) err = db.Ping() if err != nil { log.Println("Bad database configuration: %v", err) panic(err) // maybe os.Exit(1) instead } if len(args) == 0 { serve() return } switch args[0] { case "seed": dbSeed(db) case "reset": dbReset(db) default: return } db.Close() } func check(args []string) { os.Setenv("DBName", "skouter_dev") os.Setenv("DBUser", "tester") os.Setenv("DBPass", "test123") files := http.FileServer(http.Dir("")) http.Handle("/assets/", files) http.HandleFunc("/", checkPdf) log.Fatal(http.ListenAndServe(address, nil)) } func main() { if len(os.Args) <= 1 { serve() return } switch os.Args[1] { case "dev": dev(os.Args[2:]) case "checkpdf": check(os.Args[2:]) default: return } }