@@ -385,6 +385,10 @@ section.mi .row input[type=checkbox] { | |||||
position: absolute; | position: absolute; | ||||
} | } | ||||
label.error { | |||||
color: var(--text-important); | |||||
} | |||||
section.estimates .entry { | section.estimates .entry { | ||||
padding: 10px 3px; | padding: 10px 3px; | ||||
border-bottom: 2px solid var(--outline); | border-bottom: 2px solid var(--outline); | ||||
@@ -26,7 +26,11 @@ v-else-if="active == 3" | |||||
@removeFeeTemp="(fee) => fees = fees.filter(f => f.id != fee.id)" | @removeFeeTemp="(fee) => fees = fees.filter(f => f.id != fee.id)" | ||||
/> | /> | ||||
<settings :user="user" v-else-if="active == 4" /> | |||||
<settings | |||||
:user="user" | |||||
:token="token" | |||||
@updateAvatar="updateAvatar" | |||||
v-else-if="active == 4" /> | |||||
<sign-out :user="user" v-else-if="active == 5" /> | <sign-out :user="user" v-else-if="active == 5" /> | ||||
</template> | </template> | ||||
@@ -97,12 +101,36 @@ function getUser() { | |||||
this.user = result[0] | this.user = result[0] | ||||
if (this.user.avatar) return | if (this.user.avatar) return | ||||
fetch("/assets/image/empty-avatar.jpg").then(r => r.blob()). | |||||
then(b => this.user.avatar = b) | |||||
return getAvatar(token) | |||||
}).then(b => { | |||||
const validTypes = ['image/jpeg', 'image/png'] | |||||
if (!validTypes.includes(b.type) || b.size <= 1) { | |||||
fetch("/assets/image/empty-avatar.jpg"). | |||||
then(r => r.blob()).then( a => this.user.avatar = a ) | |||||
return | |||||
} | |||||
this.user.avatar = b | |||||
}) | }) | ||||
} | } | ||||
function getAvatar(t) { | |||||
return fetch("/api/user/avatar", | |||||
{method: 'GET', | |||||
headers: { | |||||
"Accept": "application/json", | |||||
"Authorization": `Bearer ${t || this.token}`, | |||||
} | |||||
}).then(r => r.blob()) | |||||
} | |||||
function updateAvatar() { | |||||
const token = getCookie("skouter") | |||||
getAvatar(token).then(b => this.user.avatar = b) | |||||
} | |||||
function getFees() { | function getFees() { | ||||
const token = getCookie("skouter") | const token = getCookie("skouter") | ||||
@@ -181,6 +209,8 @@ export default { | |||||
getUser, | getUser, | ||||
getFees, | getFees, | ||||
refreshToken, | refreshToken, | ||||
updateAvatar, | |||||
getAvatar, | |||||
}, | }, | ||||
data() { | data() { | ||||
return { | return { | ||||
@@ -6,9 +6,10 @@ | |||||
<h3>Avatar</h3> | <h3>Avatar</h3> | ||||
<canvas width="200" height="200" ref="canvas"></canvas> | <canvas width="200" height="200" ref="canvas"></canvas> | ||||
<input type="file" | <input type="file" | ||||
@change="e => changeAvatar(e.target.files[0])" | |||||
@change="e => uploadAvatar(e.target.files[0])" | |||||
/> | /> | ||||
<button>Upload</button> | <button>Upload</button> | ||||
<label class="error">{{avatarError}}</label> | |||||
</section> | </section> | ||||
<section class="form inputs"> | <section class="form inputs"> | ||||
@@ -56,14 +57,16 @@ | |||||
</template> | </template> | ||||
<script setup> | <script setup> | ||||
import { ref, watch } from "vue" | |||||
import { ref, watch, onMounted } from "vue" | |||||
import Dialog from "./dialog.vue" | import Dialog from "./dialog.vue" | ||||
let avatar = ref(null) | let avatar = ref(null) | ||||
let ready = ref(false) | let ready = ref(false) | ||||
let avatarChanged = ref(false) | let avatarChanged = ref(false) | ||||
let avatarError = ref('') | |||||
const canvas = ref(null) | const canvas = ref(null) | ||||
const props = defineProps(['user', 'token']) | const props = defineProps(['user', 'token']) | ||||
const emit = defineEmits(['updateAvatar']) | |||||
function save() { | function save() { | ||||
} | } | ||||
@@ -73,25 +76,51 @@ function check() { | |||||
} | } | ||||
function uploadAvatar(blob) { | function uploadAvatar(blob) { | ||||
changeAvatar(blob)?.then(() => { | |||||
canvas.value.toBlob(b => { | |||||
fetch(`/api/user/avatar`, | |||||
{method: 'POST', | |||||
body: b, | |||||
headers: { | |||||
"Accept": "application/json", | |||||
"Authorization": `Bearer ${props.token}`, | |||||
}, | |||||
}).then(resp => { | |||||
if (resp.ok) {emit('updateAvatar')} | |||||
}) | |||||
}) | |||||
}) | |||||
canvas.value.toBlob(b => { | |||||
// uploadAvatar(b) | |||||
}) | |||||
} | } | ||||
function changeAvatar(blob) { | function changeAvatar(blob) { | ||||
const validTypes = ['image/jpeg', 'image/png'] | |||||
if (!validTypes.includes(blob.type)) { | |||||
avatarError.value = 'Image must be JPEG of PNG format' | |||||
return | |||||
} | |||||
avatarError.value = '' | |||||
avatar.value = blob | avatar.value = blob | ||||
createImageBitmap(blob, | |||||
return createImageBitmap(blob, | |||||
{resizeWidth: 200, resizeHeight: 200, resizeQuality: 'medium'}). | {resizeWidth: 200, resizeHeight: 200, resizeQuality: 'medium'}). | ||||
then((img) => { | then((img) => { | ||||
avatar.value = img | avatar.value = img | ||||
canvas.value.getContext("2d").drawImage(img, 0, 0, 200, 200) | canvas.value.getContext("2d").drawImage(img, 0, 0, 200, 200) | ||||
canvas.value.toBlob(b => { | |||||
console.log(b) | |||||
}) | |||||
}) | }) | ||||
} | } | ||||
watch(props.user, () => { | watch(props.user, () => { | ||||
if (!props.user.avatar) return | |||||
changeAvatar(props.user.avatar) | changeAvatar(props.user.avatar) | ||||
}) | |||||
}, {immediate: true}) | |||||
</script> | </script> |
@@ -17,6 +17,7 @@ import ( | |||||
"errors" | "errors" | ||||
"strings" | "strings" | ||||
"math" | "math" | ||||
"io" | |||||
// pdf "github.com/SebastiaanKlippert/go-wkhtmltopdf" | // pdf "github.com/SebastiaanKlippert/go-wkhtmltopdf" | ||||
"github.com/golang-jwt/jwt/v4" | "github.com/golang-jwt/jwt/v4" | ||||
) | ) | ||||
@@ -959,6 +960,66 @@ func createUser(w http.ResponseWriter, db *sql.DB, r *http.Request) { | |||||
json.NewEncoder(w).Encode(user) | json.NewEncoder(w).Encode(user) | ||||
} | } | ||||
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 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 queryBorrower(db *sql.DB, id int) ( Borrower, error ) { | func queryBorrower(db *sql.DB, id int) ( Borrower, error ) { | ||||
var borrower Borrower | var borrower Borrower | ||||
var query string | var query string | ||||
@@ -1602,6 +1663,14 @@ func api(w http.ResponseWriter, r *http.Request) { | |||||
r.Method == http.MethodDelete && | r.Method == http.MethodDelete && | ||||
guard(r, 3): | guard(r, 3): | ||||
deleteUser(w, db, r) | deleteUser(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/fees", &args) && | case match(p, "/api/fees", &args) && | ||||
r.Method == http.MethodGet && | r.Method == http.MethodGet && | ||||
guard(r, 1): | guard(r, 1): | ||||