diff --git a/assets/main.css b/assets/main.css index 14ac617..021e600 100644 --- a/assets/main.css +++ b/assets/main.css @@ -385,6 +385,10 @@ section.mi .row input[type=checkbox] { position: absolute; } +label.error { + color: var(--text-important); +} + section.estimates .entry { padding: 10px 3px; border-bottom: 2px solid var(--outline); diff --git a/components/app.vue b/components/app.vue index 2a99736..9b8e70d 100644 --- a/components/app.vue +++ b/components/app.vue @@ -26,7 +26,11 @@ v-else-if="active == 3" @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" /> </template> @@ -97,12 +101,36 @@ function getUser() { this.user = result[0] 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() { const token = getCookie("skouter") @@ -181,6 +209,8 @@ export default { getUser, getFees, refreshToken, + updateAvatar, + getAvatar, }, data() { return { diff --git a/components/settings.vue b/components/settings.vue index d9eba03..b296cdd 100644 --- a/components/settings.vue +++ b/components/settings.vue @@ -6,9 +6,10 @@ <h3>Avatar</h3> <canvas width="200" height="200" ref="canvas"></canvas> <input type="file" -@change="e => changeAvatar(e.target.files[0])" +@change="e => uploadAvatar(e.target.files[0])" /> <button>Upload</button> +<label class="error">{{avatarError}}</label> </section> <section class="form inputs"> @@ -56,14 +57,16 @@ </template> <script setup> -import { ref, watch } from "vue" +import { ref, watch, onMounted } from "vue" import Dialog from "./dialog.vue" let avatar = ref(null) let ready = ref(false) let avatarChanged = ref(false) +let avatarError = ref('') const canvas = ref(null) const props = defineProps(['user', 'token']) +const emit = defineEmits(['updateAvatar']) function save() { } @@ -73,25 +76,51 @@ function check() { } 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) { + 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 - createImageBitmap(blob, + return createImageBitmap(blob, {resizeWidth: 200, resizeHeight: 200, resizeQuality: 'medium'}). then((img) => { avatar.value = img canvas.value.getContext("2d").drawImage(img, 0, 0, 200, 200) - canvas.value.toBlob(b => { - console.log(b) - }) }) } - - watch(props.user, () => { + if (!props.user.avatar) return changeAvatar(props.user.avatar) -}) +}, {immediate: true}) + </script> diff --git a/skouter.go b/skouter.go index 73a9095..5e6f3e8 100644 --- a/skouter.go +++ b/skouter.go @@ -17,6 +17,7 @@ import ( "errors" "strings" "math" + "io" // pdf "github.com/SebastiaanKlippert/go-wkhtmltopdf" "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) } +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 ) { var borrower Borrower var query string @@ -1602,6 +1663,14 @@ func api(w http.ResponseWriter, r *http.Request) { r.Method == http.MethodDelete && guard(r, 3): 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) && r.Method == http.MethodGet && guard(r, 1):