diff --git a/assets/main.css b/assets/main.css
index f562608..445fb33 100644
--- a/assets/main.css
+++ b/assets/main.css
@@ -364,3 +364,8 @@ section.special {
border-radius: 3px;
padding: 25px 10px;
}
+
+.loading span.error {
+ top: 40px;
+ position: absolute;
+}
diff --git a/components/app.vue b/components/app.vue
index fc61e15..f3bdeb7 100644
--- a/components/app.vue
+++ b/components/app.vue
@@ -1,10 +1,12 @@
-
+
+
+{{loadingError}}
@@ -13,6 +15,11 @@
+
+
+
+
+
@@ -25,13 +32,75 @@ import NewEstimate from "./new.vue"
import Estimates from "./estimates.vue"
import Settings from "./settings.vue"
import SignOut from "./sign-out.vue"
+import Login from "./login.vue"
+
+function getCookie(name) {
+ var re = new RegExp(name + "=([^;]+)")
+ var value = re.exec(document.cookie)
+
+ return (value != null) ? unescape(value[1]) : null
+}
+
+function refreshToken() {
+ const token = getCookie("skouter")
-const user = {
- firstName: "test",
- lastName: "user",
- id: 12,
- status: 1,
- }
+ fetch(`/api/token`,
+ {method: 'GET',
+ headers: {
+ "Accept": "application/json",
+ "Authorization": `Bearer ${token}`,
+ },
+ }).then(response => {
+ if (!response.ok) {
+ console.log("Error refreshing token.")
+ }
+ })
+
+ // Recursive refresh
+ setTimeout(this.refreshToken, 1000*60*25)
+}
+
+function getUser() {
+ const token = getCookie("skouter")
+
+ return fetch(`/api/user`,
+ {method: 'GET',
+ headers: {
+ "Accept": "application/json",
+ "Authorization": `Bearer ${token}`,
+ },
+ }).then(response => {
+ if (response.ok) {
+ return response.json()
+ } else {
+ // Redirect to login if starting token is invalid
+ window.location.hash = '#login'
+ }
+ }).then (result => {
+ if (!result || !result.length) return // Exit if token is invalid
+ this.user = result[0]
+ })
+
+}
+
+function getFees() {
+ const token = getCookie("skouter")
+
+ return fetch(`/api/fees`,
+ {method: 'GET',
+ headers: {
+ "Accept": "application/json",
+ "Authorization": `Bearer ${token}`,
+ },
+ }).then(response => {
+ if (response.ok) { return response.json() }
+ }).then (result => {
+ if (!result || !result.length) return // Exit if token is invalid or no fees are saved
+ this.fees = result
+ console.log("the result %O", result)
+ })
+
+}
// The default fees of a new loan. Percentage values take precedent over amounts
const fees = [
@@ -60,11 +129,35 @@ function active() {
return 4
} else if (/^#sign-out\/?/.exec(this.hash)) {
return 5
+ } else if (/^#login\/?/.exec(this.hash)) {
+ return 6
} else {
return 0
}
}
+// Fetch data before showing UI. If requests fail, assume token is expired.
+function start() {
+ this.loading = true
+
+ let loaders = []
+ loaders.push(this.getUser())
+ loaders.push(this.getFees())
+ Promise.all(loaders).then((a, b) => {
+ this.loading = false
+ if (!b) {
+ // Time untill token expiration may have elapsed before the page
+ // reloaded
+ this.refreshToken()
+ }
+ }).catch(error => {
+ console.log("An error occured %O", error)
+ this.loadingError = "Could not initialize app."
+ })
+
+}
+
+
export default {
components: {
SideBar,
@@ -73,17 +166,36 @@ export default {
NewEstimate,
Estimates,
Settings,
- SignOut
+ SignOut,
+ Login
},
computed: { active },
+ methods: {
+ getCookie,
+ start,
+ getUser,
+ getFees,
+ refreshToken,
+ },
data() {
return {
- loading: true, user: user, hash: window.location.hash,
- fees: fees
+ loading: true,
+ user: null,
+ hash: window.location.hash,
+ fees: [],
+ loadingError: "",
}
},
created() {
window.onhashchange = () => this.hash = window.location.hash
+ this.token = this.getCookie("skouter")
+
+ if (!this.token) {
+ window.location.hash = 'login'
+ this.loading = false
+ return
+ }
+ this.start()
}
}
diff --git a/components/login.vue b/components/login.vue
new file mode 100644
index 0000000..4773bc3
--- /dev/null
+++ b/components/login.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/migrations/0_29092022_setup_tables.sql b/migrations/0_29092022_setup_tables.sql
index 9b4a269..fade979 100644
--- a/migrations/0_29092022_setup_tables.sql
+++ b/migrations/0_29092022_setup_tables.sql
@@ -27,7 +27,7 @@ CREATE TABLE user (
'Subscribed',
'Branch',
'Admin'),
- role ENUM('User', 'Manager', 'Admin'),
+ role ENUM('User', 'Manager', 'Admin') NOT NULL,
PRIMARY KEY (`id`),
FOREIGN KEY (branch_id) REFERENCES branch(id)
);
diff --git a/migrations/seed.sql b/migrations/seed.sql
index a776b8b..6a3eca9 100644
--- a/migrations/seed.sql
+++ b/migrations/seed.sql
@@ -13,6 +13,7 @@ INSERT IGNORE INTO user (
title,
email,
verified,
+ role,
status
) VALUES
@@ -25,6 +26,7 @@ INSERT IGNORE INTO user (
'Loan Officer',
'test@example.com',
true,
+ 'User',
'Free'
),
@@ -37,6 +39,7 @@ INSERT IGNORE INTO user (
'Mortgage Broker',
'unverified@example.com',
false,
+ 'User',
'Free'
),
@@ -49,6 +52,7 @@ INSERT IGNORE INTO user (
'Branch Manager',
'manager@example.com',
true,
+ 'Manager',
'Free'
);
diff --git a/skouter.go b/skouter.go
index 9542148..8f721b6 100644
--- a/skouter.go
+++ b/skouter.go
@@ -2,6 +2,7 @@ package main
import (
"net/http"
+ "net/mail"
"log"
"sync"
"regexp"
@@ -15,10 +16,24 @@ import (
"time"
"errors"
"strings"
- pdf "github.com/SebastiaanKlippert/go-wkhtmltopdf"
+ // pdf "github.com/SebastiaanKlippert/go-wkhtmltopdf"
"github.com/golang-jwt/jwt/v4"
)
+type User struct {
+ Id int `json:"id"`
+ Email string `json:"email"`
+ FirstName string `json:"firstName"`
+ LastName string `json:"lastName"`
+ BranchId int `json:"branchId"`
+ 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"`
+}
+
type UserClaims struct {
Id int `json:"id"`
Role string `json:"role"`
@@ -292,14 +307,12 @@ func getFees(db *sql.DB, loan int) ([]Fee, error) {
return fees, nil
}
-// Fetch fees from the database
-func getFeesTemp(db *sql.DB, user int) ([]FeeTemplate, error) {
+func fetchFeesTemp(db *sql.DB, user int, branch int) ([]FeeTemplate, error) {
var fees []FeeTemplate
-
rows, err := db.Query(
"SELECT * FROM fee_template " +
- "WHERE user_id = ? OR user_id = 0",
- user)
+ "WHERE user_id = ? OR branch_id = ?",
+ user, branch)
if err != nil {
return nil, fmt.Errorf("Fee template query error %v", err)
@@ -331,6 +344,18 @@ func getFeesTemp(db *sql.DB, user int) ([]FeeTemplate, error) {
return fees, 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 }
+ users, err := queryUsers(db, claims.Id)
+ if err != nil { w.WriteHeader(422); return }
+
+ fees, err = fetchFeesTemp(db, claims.Id, users[0].BranchId)
+ json.NewEncoder(w).Encode(fees)
+}
+
func getMi(db *sql.DB, loan int) (MI, error) {
var mi MI
@@ -526,11 +551,13 @@ func login(w http.ResponseWriter, db *sql.DB, r *http.Request) {
var id int
var role string
var err error
- r.ParseForm()
+ var user User
+ json.NewDecoder(r.Body).Decode(&user)
row := db.QueryRow(
`SELECT id, role FROM user WHERE email = ? AND password = sha2(?, 256)`,
- r.PostFormValue("email"), r.PostFormValue("password"))
+ user.Email, user.Password,
+ )
err = row.Scan(&id, &role)
if err != nil {
@@ -549,7 +576,7 @@ func login(w http.ResponseWriter, db *sql.DB, r *http.Request) {
return
}
- cookie := http.Cookie{Name: "hound",
+ cookie := http.Cookie{Name: "skouter",
Value: tokenStr,
Path: "/",
Expires: time.Now().Add(time.Hour * 24)}
@@ -575,7 +602,7 @@ func getToken(w http.ResponseWriter, db *sql.DB, r *http.Request) {
return
}
- cookie := http.Cookie{Name: "hound",
+ cookie := http.Cookie{Name: "skouter",
Value: tokenStr,
Path: "/",
Expires: time.Now().Add(time.Hour * 24)}
@@ -640,7 +667,6 @@ func queryUsers(db *sql.DB, id int) ( []User, error ) {
u.title,
u.status,
u.verified,
- u.last_login,
u.role
FROM user u WHERE u.id = CASE @e := ? WHEN 0 THEN u.id ELSE @e END
`
@@ -666,7 +692,6 @@ func queryUsers(db *sql.DB, id int) ( []User, error ) {
&user.Title,
&user.Status,
&user.Verified,
- &user.LastLogin,
&user.Role,
)
err != nil {
@@ -748,10 +773,78 @@ func getUser(w http.ResponseWriter, db *sql.DB, r *http.Request) {
claims, err := getClaims(r)
if err != nil { w.WriteHeader(500); return }
users, err := queryUsers(db, claims.Id)
- if err != nil { w.WriteHeader(422); return }
+ if err != nil { w.WriteHeader(422); log.Println(err); return }
json.NewEncoder(w).Encode(users)
}
+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 patchUser(w http.ResponseWriter, db *sql.DB, r *http.Request) {
+ var user User
+ err := json.NewDecoder(r.Body).Decode(&user)
+
+ _, err = mail.ParseAddress(user.Email)
+ if err != nil { http.Error(w, "Invalid email.", 422); return }
+
+ if roles[user.Role] == 0 {
+ http.Error(w, "Invalid role.", 422)
+ return
+ }
+
+ err = updateUser(user, db)
+ if err != nil { http.Error(w, "Bad form values.", 422); return }
+
+ users, err := queryUsers(db, user.Id)
+ if err != nil { http.Error(w, "Bad form values.", 422); return }
+ json.NewEncoder(w).Encode(users[0])
+}
+
+// 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
+ }
+
+ patchUser(w, db, r)
+}
+
+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)
+ }
+}
+
func createUser(w http.ResponseWriter, db *sql.DB, r *http.Request) {
var user User
err := json.NewDecoder(r.Body).Decode(&user)
@@ -816,6 +909,10 @@ func api(w http.ResponseWriter, r *http.Request) {
r.Method == http.MethodDelete &&
guard(r, 3):
deleteUser(w, db, r)
+ case match(p, "/api/fees", &args) &&
+ r.Method == http.MethodGet &&
+ guard(r, 1):
+ getFeesTemp(w, db, r)
}
db.Close()