|
|
@@ -10,11 +10,21 @@ import ( |
|
|
|
_ "github.com/go-sql-driver/mysql" |
|
|
|
"fmt" |
|
|
|
"encoding/json" |
|
|
|
// "io" |
|
|
|
"strconv" |
|
|
|
"bytes" |
|
|
|
"time" |
|
|
|
"errors" |
|
|
|
"strings" |
|
|
|
pdf "github.com/SebastiaanKlippert/go-wkhtmltopdf" |
|
|
|
"github.com/golang-jwt/jwt/v4" |
|
|
|
) |
|
|
|
|
|
|
|
type UserClaims struct { |
|
|
|
Id int `json:"id"` |
|
|
|
Role string `json:"role"` |
|
|
|
Exp string `json:"exp"` |
|
|
|
} |
|
|
|
|
|
|
|
type Page struct { |
|
|
|
tpl *template.Template |
|
|
|
Title string |
|
|
@@ -75,16 +85,16 @@ type Loan struct { |
|
|
|
} |
|
|
|
|
|
|
|
type MI struct { |
|
|
|
Type string |
|
|
|
Label string |
|
|
|
Lender string |
|
|
|
Rate float32 |
|
|
|
Premium float32 |
|
|
|
Upfront float32 |
|
|
|
FiveYearTotal float32 |
|
|
|
InitialAllInPremium float32 |
|
|
|
InitialAllInRate float32 |
|
|
|
InitialAmount float32 |
|
|
|
Type string |
|
|
|
Label string |
|
|
|
Lender string |
|
|
|
Rate float32 |
|
|
|
Premium float32 |
|
|
|
Upfront float32 |
|
|
|
FiveYearTotal float32 |
|
|
|
InitialAllInPremium float32 |
|
|
|
InitialAllInRate float32 |
|
|
|
InitialAmount float32 |
|
|
|
} |
|
|
|
|
|
|
|
type Estimate struct { |
|
|
@@ -118,6 +128,22 @@ var pages = map[string]Page { |
|
|
|
"app": cache("app", "App"), |
|
|
|
} |
|
|
|
|
|
|
|
var roles = map[string]int{ |
|
|
|
"guest": 1, |
|
|
|
"employee": 2, |
|
|
|
"admin": 3, |
|
|
|
} |
|
|
|
|
|
|
|
// 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 cache(name string, title string) Page { |
|
|
|
var p = []string{"master.tpl", paths[name]} |
|
|
|
tpl := template.Must(template.ParseFiles(p...)) |
|
|
@@ -482,6 +508,7 @@ func fetchMi(db *sql.DB, estimate *Estimate, pos int) []MI { |
|
|
|
|
|
|
|
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", |
|
|
@@ -489,40 +516,116 @@ func fetchMi(db *sql.DB, estimate *Estimate, pos int) []MI { |
|
|
|
} else { |
|
|
|
json.NewDecoder(resp.Body).Decode(&res) |
|
|
|
// estimate.Loans[pos].Mi = res |
|
|
|
// Parse res into result here |
|
|
|
} |
|
|
|
|
|
|
|
return result |
|
|
|
} |
|
|
|
|
|
|
|
func login(w http.ResponseWriter, db *sql.DB, r *http.Request) { |
|
|
|
var id int |
|
|
|
var role string |
|
|
|
var err error |
|
|
|
r.ParseForm() |
|
|
|
|
|
|
|
row := db.QueryRow( |
|
|
|
`SELECT id, role FROM user WHERE email = ? AND password = sha2(?, 256)`, |
|
|
|
r.PostFormValue("email"), r.PostFormValue("password")) |
|
|
|
|
|
|
|
err = row.Scan(&id, &role) |
|
|
|
if err != nil { |
|
|
|
http.Error(w, "Invalid Credentials.", http.StatusUnauthorized) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
return estimate |
|
|
|
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(config["JWT_SECRET"])) |
|
|
|
if err != nil { |
|
|
|
log.Println("Token could not be signed: ", err, tokenStr) |
|
|
|
http.Error(w, "Token generation error.", http.StatusInternalServerError) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
cookie := http.Cookie{Name: "hound", |
|
|
|
Value: tokenStr, |
|
|
|
Path: "/", |
|
|
|
Expires: time.Now().Add(time.Hour * 24)} |
|
|
|
http.SetCookie(w, &cookie) |
|
|
|
_, err = w.Write([]byte(tokenStr)) |
|
|
|
if err != nil { |
|
|
|
http.Error(w, |
|
|
|
"Could not complete token write.", |
|
|
|
http.StatusInternalServerError)} |
|
|
|
} |
|
|
|
|
|
|
|
func getToken(w http.ResponseWriter, db *sql.DB, r *http.Request) { |
|
|
|
claims, err := getClaims(r) |
|
|
|
// Will verify existing signature and expiry time |
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, |
|
|
|
UserClaims{ Id: claims.Id, Role: claims.Role, |
|
|
|
Exp: time.Now().Add(time.Minute * 30).Format(time.UnixDate)}) |
|
|
|
|
|
|
|
tokenStr, err := token.SignedString([]byte(config["JWT_SECRET"])) |
|
|
|
if err != nil { |
|
|
|
log.Println("Token could not be signed: ", err, tokenStr) |
|
|
|
http.Error(w, "Token generation error.", http.StatusInternalServerError) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
cookie := http.Cookie{Name: "hound", |
|
|
|
Value: tokenStr, |
|
|
|
Path: "/", |
|
|
|
Expires: time.Now().Add(time.Hour * 24)} |
|
|
|
http.SetCookie(w, &cookie) |
|
|
|
_, err = w.Write([]byte(tokenStr)) |
|
|
|
if err != nil { |
|
|
|
http.Error(w, |
|
|
|
"Could not complete token write.", |
|
|
|
http.StatusInternalServerError)} |
|
|
|
} |
|
|
|
|
|
|
|
func validateEstimate() { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
func route(w http.ResponseWriter, r *http.Request) { |
|
|
|
var page Page |
|
|
|
var args []string |
|
|
|
p := r.URL.Path |
|
|
|
func getClaims(r *http.Request) (UserClaims, error) { |
|
|
|
claims := new(UserClaims) |
|
|
|
_, tokenStr, found := strings.Cut(r.Header.Get("Authorization"), " ") |
|
|
|
|
|
|
|
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" ] |
|
|
|
case match(p, "/assets", &args): |
|
|
|
page = pages[ "app" ] |
|
|
|
default: |
|
|
|
http.NotFound(w, r) |
|
|
|
return |
|
|
|
} |
|
|
|
if !found { |
|
|
|
return *claims, errors.New("Token not found") |
|
|
|
} |
|
|
|
|
|
|
|
page.Render(w) |
|
|
|
// Pull token payload into UserClaims |
|
|
|
_, err := jwt.ParseWithClaims(tokenStr, claims, |
|
|
|
func(token *jwt.Token) (any, error) { |
|
|
|
return []byte(config["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 |
|
|
|
} |
|
|
|
|
|
|
|
func api(w http.ResponseWriter, r *http.Request) { |
|
|
|
var args []string |
|
|
|
// var response string |
|
|
|
|
|
|
|
p := r.URL.Path |
|
|
|
db, err := sql.Open("mysql", |
|
|
@@ -534,54 +637,114 @@ func api(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
|
|
|
err = db.Ping() |
|
|
|
if err != nil { |
|
|
|
print("Bad database configuration: %v", err) |
|
|
|
fmt.Println("Bad database configuration: %v\n", err) |
|
|
|
panic(err) |
|
|
|
// maybe os.Exit(1) instead |
|
|
|
} |
|
|
|
|
|
|
|
switch { |
|
|
|
case match(p, "/api/loans", &args): |
|
|
|
resp, err := getLoanType(db, 0, 0, true) |
|
|
|
|
|
|
|
if resp != nil { |
|
|
|
json.NewEncoder(w).Encode(resp) |
|
|
|
} else { |
|
|
|
json.NewEncoder(w).Encode(err) |
|
|
|
} |
|
|
|
|
|
|
|
case match(p, "/api/fees", &args): |
|
|
|
resp, err := getFeesTemp(db, 0) |
|
|
|
|
|
|
|
if resp != nil { |
|
|
|
json.NewEncoder(w).Encode(resp) |
|
|
|
} else { |
|
|
|
json.NewEncoder(w).Encode(err) |
|
|
|
} |
|
|
|
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/users", &args) && // Array of all users |
|
|
|
r.Method == http.MethodGet && guard(r, 2): |
|
|
|
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 && |
|
|
|
guard(r, 3): |
|
|
|
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, 2): // 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/batch", &args) && |
|
|
|
r.Method == http.MethodGet && |
|
|
|
guard(r, 1): |
|
|
|
getBatch(w, db, r) |
|
|
|
case match(p, "/api/batch", &args) && |
|
|
|
r.Method == http.MethodPost && |
|
|
|
guard(r, 2): |
|
|
|
openBatch(w, db, r) |
|
|
|
case match(p, "/api/batch", &args) && |
|
|
|
r.Method == http.MethodPatch && |
|
|
|
guard(r, 2): |
|
|
|
closeBatch(w, db, r) |
|
|
|
case match(p, "/api/client", &args) && |
|
|
|
r.Method == http.MethodGet && |
|
|
|
guard(r, 1): |
|
|
|
getClient(w, db, r) |
|
|
|
case match(p, "/api/client", &args) && |
|
|
|
r.Method == http.MethodPost && |
|
|
|
guard(r, 2): |
|
|
|
createClient(w, db, r) |
|
|
|
case match(p, "/api/client", &args) && |
|
|
|
r.Method == http.MethodPatch && |
|
|
|
guard(r, 2): |
|
|
|
patchClient(w, db, r) |
|
|
|
case match(p, "/api/client", &args) && |
|
|
|
r.Method == http.MethodDelete && |
|
|
|
guard(r, 2): |
|
|
|
deleteClient(w, db, r) |
|
|
|
case match(p, "/api/ticket", &args) && |
|
|
|
r.Method == http.MethodPost && |
|
|
|
guard(r, 2): |
|
|
|
openTicket(w, db, r) |
|
|
|
case match(p, "/api/ticket", &args) && |
|
|
|
r.Method == http.MethodPatch && |
|
|
|
guard(r, 2): |
|
|
|
closeTicket(w, db, r) |
|
|
|
case match(p, "/api/ticket", &args) && |
|
|
|
r.Method == http.MethodDelete && |
|
|
|
guard(r, 2): |
|
|
|
voidTicket(w, db, r) |
|
|
|
case match(p, "/api/report/batch", &args) && |
|
|
|
r.Method == http.MethodGet && |
|
|
|
guard(r, 2): |
|
|
|
reportBatch(w, db, r) |
|
|
|
case match(p, "/api/report/summary", &args) && |
|
|
|
r.Method == http.MethodPost && |
|
|
|
guard(r, 2): |
|
|
|
reportSummary(w, db, r) |
|
|
|
} |
|
|
|
|
|
|
|
case match(p, "/api/mi", &args): |
|
|
|
var err error |
|
|
|
est, err := getEstimate(db, 1) |
|
|
|
if err != nil { |
|
|
|
json.NewEncoder(w).Encode(err) |
|
|
|
log.Println("error occured:", err) |
|
|
|
break |
|
|
|
} |
|
|
|
db.Close() |
|
|
|
} |
|
|
|
|
|
|
|
json.NewEncoder(w).Encode(fetchMi(db, &est, 0).Loans[0].Mi) |
|
|
|
func route(w http.ResponseWriter, r *http.Request) { |
|
|
|
var page Page |
|
|
|
var args []string |
|
|
|
p := r.URL.Path |
|
|
|
|
|
|
|
// if err != nil { |
|
|
|
// json.NewEncoder(w).Encode(err) |
|
|
|
// break |
|
|
|
// } else { |
|
|
|
// json.NewEncoder(w).Encode(resp) |
|
|
|
// } |
|
|
|
} |
|
|
|
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 main() { |
|
|
|
files := http.FileServer(http.Dir("")) |
|
|
|
|
|
|
|
|
|
|
|
http.Handle("/assets/", files) |
|
|
|
http.HandleFunc("/api/", api) |
|
|
|