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 @@
 <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>
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 @@
+<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>
\ 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()