mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 06:23:34 -07:00
profile image upload
This commit is contained in:
parent
2c862806ba
commit
1f16d0cb39
9 changed files with 127 additions and 24 deletions
|
@ -12,7 +12,6 @@ const categoryURLs = {
|
|||
export default {
|
||||
async get_all() {
|
||||
let response = await apiReq.get(categoryURLs.get_all);
|
||||
console.log("All Cats", response.data);
|
||||
return response.data;
|
||||
},
|
||||
async get_recipes_in_category(category) {
|
||||
|
|
|
@ -117,7 +117,6 @@ export default {
|
|||
},
|
||||
async mounted() {
|
||||
let settings = await api.settings.requestAll();
|
||||
console.log("Settings", settings.planCategories);
|
||||
this.items = await api.recipes.getAllByCategory(settings.planCategories);
|
||||
},
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ export default {
|
|||
url: String,
|
||||
text: { default: "Upload" },
|
||||
icon: { default: "mdi-cloud-upload" },
|
||||
fileName: { defaul: "archive" },
|
||||
},
|
||||
data: () => ({
|
||||
file: null,
|
||||
|
@ -32,7 +33,7 @@ export default {
|
|||
if (this.file != null) {
|
||||
this.isSelecting = true;
|
||||
let formData = new FormData();
|
||||
formData.append("archive", this.file);
|
||||
formData.append(this.fileName, this.file);
|
||||
|
||||
await api.utils.uploadFile(this.url, formData);
|
||||
|
||||
|
|
17
frontend/src/mixins/initials.js
Normal file
17
frontend/src/mixins/initials.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
export const initials = {
|
||||
computed: {
|
||||
initials() {
|
||||
const allNames = this.user.fullName.trim().split(" ");
|
||||
const initials = allNames.reduce(
|
||||
(acc, curr, index) => {
|
||||
if (index === 0 || index === allNames.length - 1) {
|
||||
acc = `${acc}${curr.charAt(0).toUpperCase()}`;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[""]
|
||||
);
|
||||
return initials;
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,6 +1,5 @@
|
|||
import { store } from "@/store";
|
||||
export const user = {
|
||||
data() {},
|
||||
computed: {
|
||||
user() {
|
||||
return store.getters.getUserData;
|
||||
|
@ -8,5 +7,18 @@ export const user = {
|
|||
loggedIn() {
|
||||
return store.getters.getIsLoggedIn;
|
||||
},
|
||||
initials() {
|
||||
const allNames = this.user.fullName.trim().split(" ");
|
||||
const initials = allNames.reduce(
|
||||
(acc, curr, index) => {
|
||||
if (index === 0 || index === allNames.length - 1) {
|
||||
acc = `${acc}${curr.charAt(0).toUpperCase()}`;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[""]
|
||||
);
|
||||
return initials;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -33,18 +33,22 @@
|
|||
</v-app-bar>
|
||||
|
||||
<v-card-text>
|
||||
<v-container>
|
||||
<v-form ref="newUser">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.fullName"
|
||||
label="Full Name"
|
||||
:rules="[existsRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.email"
|
||||
label="Email"
|
||||
:rules="[existsRule, emailRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
|
@ -57,6 +61,7 @@
|
|||
<v-text-field
|
||||
v-model="editedItem.password"
|
||||
label="User Password"
|
||||
:rules="[existsRule, minRule]"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
|
@ -66,7 +71,7 @@
|
|||
></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
|
@ -121,8 +126,10 @@
|
|||
<script>
|
||||
import Confirmation from "@/components/UI/Confirmation";
|
||||
import api from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
export default {
|
||||
components: { Confirmation },
|
||||
mixins: [validators],
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
activeId: null,
|
||||
|
@ -145,6 +152,7 @@ export default {
|
|||
editedItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
|
@ -152,6 +160,7 @@ export default {
|
|||
defaultItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
|
@ -228,11 +237,12 @@ export default {
|
|||
async save() {
|
||||
if (this.editedIndex > -1) {
|
||||
api.users.update(this.editedItem);
|
||||
} else {
|
||||
this.close();
|
||||
} else if (this.$refs.newUser.validate()) {
|
||||
api.users.create(this.editedItem);
|
||||
this.close();
|
||||
}
|
||||
await this.initialize();
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -21,11 +21,20 @@
|
|||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="3" align="center" justify="center">
|
||||
<v-avatar color="accent" size="120" class="mr-2" v-if="!loading">
|
||||
<v-avatar
|
||||
color="accent"
|
||||
size="120"
|
||||
v-if="!loading"
|
||||
class="white--text headline mr-2"
|
||||
>
|
||||
<img
|
||||
src="https://cdn.vuetifyjs.com/images/john.jpg"
|
||||
alt="John"
|
||||
:src="userProfileImage"
|
||||
v-if="!hideImage"
|
||||
@error="hideImage = true"
|
||||
/>
|
||||
<div v-else>
|
||||
{{ initials }}
|
||||
</div>
|
||||
</v-avatar>
|
||||
</v-col>
|
||||
<v-col cols="12" md="9">
|
||||
|
@ -60,7 +69,12 @@
|
|||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<UploadBtn icon="mdi-image-area" text="Upload Photo" />
|
||||
<UploadBtn
|
||||
icon="mdi-image-area"
|
||||
text="Upload Photo"
|
||||
:url="userProfileImage"
|
||||
file-name="profile_image"
|
||||
/>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" class="mr-2" @click="updateUser">
|
||||
|
@ -110,17 +124,16 @@
|
|||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn icon @click="showPassword = !showPassword" :loading="passwordLoading">
|
||||
<v-btn
|
||||
icon
|
||||
@click="showPassword = !showPassword"
|
||||
:loading="passwordLoading"
|
||||
>
|
||||
<v-icon v-if="!showPassword">mdi-eye-off</v-icon>
|
||||
<v-icon v-else> mdi-eye </v-icon>
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="accent"
|
||||
class="mr-2"
|
||||
@click="changePassword"
|
||||
|
||||
>
|
||||
<v-btn color="accent" class="mr-2" @click="changePassword">
|
||||
<v-icon left> mdi-lock </v-icon>
|
||||
{{ $t("settings.change-password") }}
|
||||
</v-btn>
|
||||
|
@ -135,13 +148,15 @@
|
|||
import UploadBtn from "@/components/UI/UploadBtn";
|
||||
import api from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
import { initials } from "@/mixins/initials";
|
||||
export default {
|
||||
components: {
|
||||
UploadBtn,
|
||||
},
|
||||
mixins: [validators],
|
||||
mixins: [validators, initials],
|
||||
data() {
|
||||
return {
|
||||
hideImage: false,
|
||||
passwordLoading: false,
|
||||
password: {
|
||||
current: "",
|
||||
|
@ -160,6 +175,12 @@ export default {
|
|||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
userProfileImage() {
|
||||
return `api/users/${this.user.id}/image`;
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.refreshProfile();
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@ def ensure_dirs():
|
|||
|
||||
|
||||
# Register ENV
|
||||
ENV = CWD.joinpath(".env") #! I'm Broken Fix Me!
|
||||
ENV = CWD.joinpath(".env") #! I'm Broken Fix Me!
|
||||
dotenv.load_dotenv(ENV)
|
||||
|
||||
|
||||
|
@ -45,6 +45,7 @@ MIGRATION_DIR = DATA_DIR.joinpath("migration")
|
|||
NEXTCLOUD_DIR = MIGRATION_DIR.joinpath("nextcloud")
|
||||
CHOWDOWN_DIR = MIGRATION_DIR.joinpath("chowdown")
|
||||
TEMPLATE_DIR = DATA_DIR.joinpath("templates")
|
||||
USER_DIR = DATA_DIR.joinpath("users")
|
||||
SQLITE_DIR = DATA_DIR.joinpath("db")
|
||||
RECIPE_DATA_DIR = DATA_DIR.joinpath("recipes")
|
||||
TEMP_DIR = DATA_DIR.joinpath(".temp")
|
||||
|
@ -59,7 +60,8 @@ REQUIRED_DIRS = [
|
|||
SQLITE_DIR,
|
||||
NEXTCLOUD_DIR,
|
||||
CHOWDOWN_DIR,
|
||||
RECIPE_DATA_DIR
|
||||
RECIPE_DATA_DIR,
|
||||
USER_DIR,
|
||||
]
|
||||
|
||||
ensure_dirs()
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import shutil
|
||||
from datetime import timedelta
|
||||
|
||||
from core.config import USER_DIR
|
||||
from core.security import get_password_hash, verify_password
|
||||
from db.database import db
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from routes.deps import manager, query_user
|
||||
from fastapi import APIRouter, Depends, File, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from routes.deps import manager
|
||||
from schema.snackbar import SnackResponse
|
||||
from schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
@ -74,6 +77,45 @@ async def update_user(
|
|||
return SnackResponse.success("User Updated", access_token)
|
||||
|
||||
|
||||
@router.get("/{id}/image")
|
||||
async def get_user_image(id: str):
|
||||
""" Returns a users profile picture """
|
||||
user_dir = USER_DIR.joinpath(id)
|
||||
for recipe_image in user_dir.glob("profile_image.*"):
|
||||
print(recipe_image)
|
||||
return FileResponse(recipe_image)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
@router.post("/{id}/image")
|
||||
async def update_user_image(
|
||||
id: str,
|
||||
profile_image: UploadFile = File(...),
|
||||
current_user: UserInDB = Depends(manager),
|
||||
):
|
||||
""" Updates a User Image """
|
||||
|
||||
extension = profile_image.filename.split(".")[-1]
|
||||
|
||||
USER_DIR.joinpath(id).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
[x.unlink() for x in USER_DIR.join(id).glob("profile_image.*")]
|
||||
except:
|
||||
pass
|
||||
|
||||
dest = USER_DIR.joinpath(id, f"profile_image.{extension}")
|
||||
|
||||
with dest.open("wb") as buffer:
|
||||
shutil.copyfileobj(profile_image.file, buffer)
|
||||
|
||||
if dest.is_file:
|
||||
return SnackResponse.success("Backup uploaded")
|
||||
else:
|
||||
return SnackResponse.error("Failure uploading file")
|
||||
|
||||
|
||||
@router.put("/{id}/password")
|
||||
async def update_password(
|
||||
id: int,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue