mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -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 {
|
export default {
|
||||||
async get_all() {
|
async get_all() {
|
||||||
let response = await apiReq.get(categoryURLs.get_all);
|
let response = await apiReq.get(categoryURLs.get_all);
|
||||||
console.log("All Cats", response.data);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
async get_recipes_in_category(category) {
|
async get_recipes_in_category(category) {
|
||||||
|
|
|
@ -117,7 +117,6 @@ export default {
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
let settings = await api.settings.requestAll();
|
let settings = await api.settings.requestAll();
|
||||||
console.log("Settings", settings.planCategories);
|
|
||||||
this.items = await api.recipes.getAllByCategory(settings.planCategories);
|
this.items = await api.recipes.getAllByCategory(settings.planCategories);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ export default {
|
||||||
url: String,
|
url: String,
|
||||||
text: { default: "Upload" },
|
text: { default: "Upload" },
|
||||||
icon: { default: "mdi-cloud-upload" },
|
icon: { default: "mdi-cloud-upload" },
|
||||||
|
fileName: { defaul: "archive" },
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
file: null,
|
file: null,
|
||||||
|
@ -32,7 +33,7 @@ export default {
|
||||||
if (this.file != null) {
|
if (this.file != null) {
|
||||||
this.isSelecting = true;
|
this.isSelecting = true;
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
formData.append("archive", this.file);
|
formData.append(this.fileName, this.file);
|
||||||
|
|
||||||
await api.utils.uploadFile(this.url, formData);
|
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";
|
import { store } from "@/store";
|
||||||
export const user = {
|
export const user = {
|
||||||
data() {},
|
|
||||||
computed: {
|
computed: {
|
||||||
user() {
|
user() {
|
||||||
return store.getters.getUserData;
|
return store.getters.getUserData;
|
||||||
|
@ -8,5 +7,18 @@ export const user = {
|
||||||
loggedIn() {
|
loggedIn() {
|
||||||
return store.getters.getIsLoggedIn;
|
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-app-bar>
|
||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-container>
|
<v-form ref="newUser">
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" sm="12" md="6">
|
<v-col cols="12" sm="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="editedItem.fullName"
|
v-model="editedItem.fullName"
|
||||||
label="Full Name"
|
label="Full Name"
|
||||||
|
:rules="[existsRule]"
|
||||||
|
validate-on-blur
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" sm="12" md="6">
|
<v-col cols="12" sm="12" md="6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="editedItem.email"
|
v-model="editedItem.email"
|
||||||
label="Email"
|
label="Email"
|
||||||
|
:rules="[existsRule, emailRule]"
|
||||||
|
validate-on-blur
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" sm="12" md="6">
|
<v-col cols="12" sm="12" md="6">
|
||||||
|
@ -57,6 +61,7 @@
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="editedItem.password"
|
v-model="editedItem.password"
|
||||||
label="User Password"
|
label="User Password"
|
||||||
|
:rules="[existsRule, minRule]"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" sm="12" md="3">
|
<v-col cols="12" sm="12" md="3">
|
||||||
|
@ -66,7 +71,7 @@
|
||||||
></v-switch>
|
></v-switch>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
|
@ -121,8 +126,10 @@
|
||||||
<script>
|
<script>
|
||||||
import Confirmation from "@/components/UI/Confirmation";
|
import Confirmation from "@/components/UI/Confirmation";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
|
import { validators } from "@/mixins/validators";
|
||||||
export default {
|
export default {
|
||||||
components: { Confirmation },
|
components: { Confirmation },
|
||||||
|
mixins: [validators],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
dialog: false,
|
dialog: false,
|
||||||
activeId: null,
|
activeId: null,
|
||||||
|
@ -145,6 +152,7 @@ export default {
|
||||||
editedItem: {
|
editedItem: {
|
||||||
id: 0,
|
id: 0,
|
||||||
fullName: "",
|
fullName: "",
|
||||||
|
password: "",
|
||||||
email: "",
|
email: "",
|
||||||
family: "",
|
family: "",
|
||||||
admin: false,
|
admin: false,
|
||||||
|
@ -152,6 +160,7 @@ export default {
|
||||||
defaultItem: {
|
defaultItem: {
|
||||||
id: 0,
|
id: 0,
|
||||||
fullName: "",
|
fullName: "",
|
||||||
|
password: "",
|
||||||
email: "",
|
email: "",
|
||||||
family: "",
|
family: "",
|
||||||
admin: false,
|
admin: false,
|
||||||
|
@ -228,11 +237,12 @@ export default {
|
||||||
async save() {
|
async save() {
|
||||||
if (this.editedIndex > -1) {
|
if (this.editedIndex > -1) {
|
||||||
api.users.update(this.editedItem);
|
api.users.update(this.editedItem);
|
||||||
} else {
|
this.close();
|
||||||
|
} else if (this.$refs.newUser.validate()) {
|
||||||
api.users.create(this.editedItem);
|
api.users.create(this.editedItem);
|
||||||
|
this.close();
|
||||||
}
|
}
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
this.close();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,11 +21,20 @@
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" md="3" align="center" justify="center">
|
<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
|
<img
|
||||||
src="https://cdn.vuetifyjs.com/images/john.jpg"
|
:src="userProfileImage"
|
||||||
alt="John"
|
v-if="!hideImage"
|
||||||
|
@error="hideImage = true"
|
||||||
/>
|
/>
|
||||||
|
<div v-else>
|
||||||
|
{{ initials }}
|
||||||
|
</div>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="9">
|
<v-col cols="12" md="9">
|
||||||
|
@ -60,7 +69,12 @@
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-actions>
|
<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-spacer></v-spacer>
|
||||||
<v-btn color="success" class="mr-2" @click="updateUser">
|
<v-btn color="success" class="mr-2" @click="updateUser">
|
||||||
|
@ -110,17 +124,16 @@
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<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-if="!showPassword">mdi-eye-off</v-icon>
|
||||||
<v-icon v-else> mdi-eye </v-icon>
|
<v-icon v-else> mdi-eye </v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn
|
<v-btn color="accent" class="mr-2" @click="changePassword">
|
||||||
color="accent"
|
|
||||||
class="mr-2"
|
|
||||||
@click="changePassword"
|
|
||||||
|
|
||||||
>
|
|
||||||
<v-icon left> mdi-lock </v-icon>
|
<v-icon left> mdi-lock </v-icon>
|
||||||
{{ $t("settings.change-password") }}
|
{{ $t("settings.change-password") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
@ -135,13 +148,15 @@
|
||||||
import UploadBtn from "@/components/UI/UploadBtn";
|
import UploadBtn from "@/components/UI/UploadBtn";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import { validators } from "@/mixins/validators";
|
import { validators } from "@/mixins/validators";
|
||||||
|
import { initials } from "@/mixins/initials";
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
UploadBtn,
|
UploadBtn,
|
||||||
},
|
},
|
||||||
mixins: [validators],
|
mixins: [validators, initials],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
hideImage: false,
|
||||||
passwordLoading: false,
|
passwordLoading: false,
|
||||||
password: {
|
password: {
|
||||||
current: "",
|
current: "",
|
||||||
|
@ -160,6 +175,12 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
userProfileImage() {
|
||||||
|
return `api/users/${this.user.id}/image`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.refreshProfile();
|
this.refreshProfile();
|
||||||
},
|
},
|
||||||
|
|
|
@ -12,7 +12,7 @@ def ensure_dirs():
|
||||||
|
|
||||||
|
|
||||||
# Register ENV
|
# Register ENV
|
||||||
ENV = CWD.joinpath(".env") #! I'm Broken Fix Me!
|
ENV = CWD.joinpath(".env") #! I'm Broken Fix Me!
|
||||||
dotenv.load_dotenv(ENV)
|
dotenv.load_dotenv(ENV)
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@ MIGRATION_DIR = DATA_DIR.joinpath("migration")
|
||||||
NEXTCLOUD_DIR = MIGRATION_DIR.joinpath("nextcloud")
|
NEXTCLOUD_DIR = MIGRATION_DIR.joinpath("nextcloud")
|
||||||
CHOWDOWN_DIR = MIGRATION_DIR.joinpath("chowdown")
|
CHOWDOWN_DIR = MIGRATION_DIR.joinpath("chowdown")
|
||||||
TEMPLATE_DIR = DATA_DIR.joinpath("templates")
|
TEMPLATE_DIR = DATA_DIR.joinpath("templates")
|
||||||
|
USER_DIR = DATA_DIR.joinpath("users")
|
||||||
SQLITE_DIR = DATA_DIR.joinpath("db")
|
SQLITE_DIR = DATA_DIR.joinpath("db")
|
||||||
RECIPE_DATA_DIR = DATA_DIR.joinpath("recipes")
|
RECIPE_DATA_DIR = DATA_DIR.joinpath("recipes")
|
||||||
TEMP_DIR = DATA_DIR.joinpath(".temp")
|
TEMP_DIR = DATA_DIR.joinpath(".temp")
|
||||||
|
@ -59,7 +60,8 @@ REQUIRED_DIRS = [
|
||||||
SQLITE_DIR,
|
SQLITE_DIR,
|
||||||
NEXTCLOUD_DIR,
|
NEXTCLOUD_DIR,
|
||||||
CHOWDOWN_DIR,
|
CHOWDOWN_DIR,
|
||||||
RECIPE_DATA_DIR
|
RECIPE_DATA_DIR,
|
||||||
|
USER_DIR,
|
||||||
]
|
]
|
||||||
|
|
||||||
ensure_dirs()
|
ensure_dirs()
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
|
import shutil
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from core.config import USER_DIR
|
||||||
from core.security import get_password_hash, verify_password
|
from core.security import get_password_hash, verify_password
|
||||||
from db.database import db
|
from db.database import db
|
||||||
from db.db_setup import generate_session
|
from db.db_setup import generate_session
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, File, UploadFile
|
||||||
from routes.deps import manager, query_user
|
from fastapi.responses import FileResponse
|
||||||
|
from routes.deps import manager
|
||||||
from schema.snackbar import SnackResponse
|
from schema.snackbar import SnackResponse
|
||||||
from schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
|
from schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
|
@ -74,6 +77,45 @@ async def update_user(
|
||||||
return SnackResponse.success("User Updated", access_token)
|
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")
|
@router.put("/{id}/password")
|
||||||
async def update_password(
|
async def update_password(
|
||||||
id: int,
|
id: int,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue