@@ -364,3 +364,8 @@ section.special { | |||
border-radius: 3px; | |||
padding: 25px 10px; | |||
} | |||
.loading span.error { | |||
top: 40px; | |||
position: absolute; | |||
} |
@@ -1,10 +1,12 @@ | |||
<template> | |||
<div class="panel"> | |||
<side-bar :role="user.status" :active="active"> | |||
<template v-if="user"> | |||
<side-bar v-if="user" :role="user && user.status" :active="active"> | |||
</side-bar> | |||
<div v-if="loading" class="page loading"> | |||
<span class="error" >{{loadingError}}</span> | |||
<spinner></spinner> | |||
</div> | |||
@@ -13,6 +15,11 @@ | |||
<estimates :user="user" :fees="fees" v-else-if="active == 3" /> | |||
<settings :user="user" v-else-if="active == 4" /> | |||
<sign-out :user="user" v-else-if="active == 5" /> | |||
</template> | |||
<template v-if="!user && active == 6"> | |||
<login @login="start" /> | |||
</template> | |||
</div> | |||
</template> | |||
@@ -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() | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,44 @@ | |||
<template> | |||
<div class="page"> | |||
<section class="form inputs"> | |||
<h3>Login</h3> | |||
<label>Email</label> | |||
<input v-model="email" required> | |||
<label>Password</label> | |||
<input v-model="password" type="password" required> | |||
<button @click="login">Login</button> | |||
<div class="error"> {{error}} </div> | |||
</section> | |||
</div> | |||
</template> | |||
<script> | |||
function login() { | |||
this.error = "" | |||
return fetch(`/api/login`, | |||
{method: 'POST', | |||
body: JSON.stringify( {email: this.email, password: this.password} ), | |||
}).then(response => { | |||
if (response.ok) { | |||
return response.text() | |||
} else { | |||
this.error = "Invalid credentials" | |||
} | |||
}).then(result => { | |||
if (!result || !result.length) return // Exit if there is no token | |||
this.$emit('login') | |||
window.location.hash = '' | |||
}) | |||
} | |||
export default { | |||
emits: [ 'login' ], | |||
methods: { login }, | |||
data() { | |||
return { email: "", password: "", error: ""} | |||
}, | |||
} | |||
</script> |
@@ -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) | |||
); | |||
@@ -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' | |||
); | |||
@@ -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() | |||