mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
password reset
This commit is contained in:
parent
5f5e2e47f7
commit
7024861397
14 changed files with 257 additions and 67 deletions
|
@ -11,6 +11,7 @@ const usersURLs = {
|
|||
users: `${userPrefix}`,
|
||||
self: `${userPrefix}/self`,
|
||||
userID: id => `${userPrefix}/${id}`,
|
||||
password: id => `${userPrefix}/${id}/password`,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -42,6 +43,10 @@ export default {
|
|||
let response = await apiReq.put(usersURLs.userID(user.id), user);
|
||||
return response.data;
|
||||
},
|
||||
async changePassword(id, password) {
|
||||
let response = await apiReq.put(usersURLs.password(id), password);
|
||||
return response.data;
|
||||
},
|
||||
async delete(id) {
|
||||
let response = await apiReq.delete(usersURLs.userID(id));
|
||||
return response.data;
|
||||
|
|
|
@ -27,8 +27,10 @@
|
|||
</v-list-item-avatar>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Jane Smith</v-list-item-title>
|
||||
<v-list-item-subtitle>Admin</v-list-item-subtitle>
|
||||
<v-list-item-title> {{ user.fullName }}</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ user.admin ? "Admin" : "User" }}</v-list-item-subtitle
|
||||
>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
@ -50,7 +52,7 @@
|
|||
</v-list>
|
||||
|
||||
<v-divider></v-divider>
|
||||
<v-list nav dense>
|
||||
<v-list nav dense v-if="user.admin">
|
||||
<v-list-item
|
||||
v-for="nav in superLinks"
|
||||
:key="nav.title"
|
||||
|
@ -115,11 +117,17 @@ export default {
|
|||
],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
this.mobile = this.viewScale();
|
||||
this.showSidebar = !this.viewScale();
|
||||
},
|
||||
|
||||
computed: {
|
||||
user() {
|
||||
return this.$store.getters.getUserData;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
viewScale() {
|
||||
switch (this.$vuetify.breakpoint.name) {
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<template>
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<h2 class="mt-1 mb-1">{{$t('settings.homepage.home-page')}}</h2>
|
||||
<h2 class="mt-1 mb-1">{{ $t("settings.homepage.home-page") }}</h2>
|
||||
<v-row align="center" justify="center" dense class="mb-n7 pb-n5">
|
||||
<v-col cols="12" sm="3" md="2">
|
||||
<v-switch v-model="showRecent" :label="$t('settings.homepage.show-recent')"></v-switch>
|
||||
<v-switch
|
||||
v-model="showRecent"
|
||||
:label="$t('settings.homepage.show-recent')"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="5" md="5">
|
||||
<v-slider
|
||||
|
|
|
@ -133,8 +133,11 @@ export default {
|
|||
this.$emit("logged-in");
|
||||
this.clear();
|
||||
}
|
||||
console.log(key);
|
||||
this.$store.commit("setToken", key.data.access_token)
|
||||
this.$store.commit("setToken", key.data.access_token);
|
||||
|
||||
let user = await api.users.self();
|
||||
this.$store.commit("setUserData", user);
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -186,7 +186,7 @@ export default {
|
|||
},
|
||||
|
||||
async deleteUser() {
|
||||
await api.users.delete(this.editedIndex);
|
||||
await api.users.delete(this.activeId);
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
|
@ -227,7 +227,6 @@ export default {
|
|||
|
||||
async save() {
|
||||
if (this.editedIndex > -1) {
|
||||
console.log("New User", this.editedItem);
|
||||
api.users.update(this.editedItem);
|
||||
} else {
|
||||
api.users.create(this.editedItem);
|
||||
|
|
|
@ -1,50 +1,98 @@
|
|||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
<span>
|
||||
<v-avatar color="accent" size="40" class="mr-2" v-if="!loading">
|
||||
<img src="https://cdn.vuetifyjs.com/images/john.jpg" alt="John" />
|
||||
</v-avatar>
|
||||
<v-progress-circular
|
||||
v-else
|
||||
indeterminate
|
||||
color="primary"
|
||||
large
|
||||
class="mr-2"
|
||||
>
|
||||
</v-progress-circular>
|
||||
</span>
|
||||
Profile
|
||||
<v-spacer></v-spacer>
|
||||
User ID: {{ user.id }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field label="Full Name" v-model="user.fullName"> </v-text-field>
|
||||
<v-text-field label="Email" v-model="user.email"> </v-text-field>
|
||||
<v-text-field
|
||||
label="Family"
|
||||
readonly
|
||||
v-model="user.family"
|
||||
persistent-hint
|
||||
hint="Family groups can only be set by administrators"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn color="accent" class="mr-2">
|
||||
<v-icon left> mdi-lock </v-icon>
|
||||
{{ $t("settings.change-password") }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" class="mr-2" @click="updateUser">
|
||||
<v-icon left> mdi-content-save </v-icon>
|
||||
{{ $t("general.save") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<v-row dense>
|
||||
<v-col cols="12" md="12" sm="12">
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
<span>
|
||||
<v-avatar color="accent" size="40" class="mr-2" v-if="!loading">
|
||||
<img src="https://cdn.vuetifyjs.com/images/john.jpg" alt="John" />
|
||||
</v-avatar>
|
||||
<v-progress-circular
|
||||
v-else
|
||||
indeterminate
|
||||
color="primary"
|
||||
large
|
||||
class="mr-2"
|
||||
>
|
||||
</v-progress-circular>
|
||||
</span>
|
||||
Profile
|
||||
<v-spacer></v-spacer>
|
||||
User ID: {{ user.id }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field label="Full Name" v-model="user.fullName">
|
||||
</v-text-field>
|
||||
<v-text-field label="Email" v-model="user.email"> </v-text-field>
|
||||
<v-text-field
|
||||
label="Family"
|
||||
readonly
|
||||
v-model="user.family"
|
||||
persistent-hint
|
||||
hint="Family groups can only be set by administrators"
|
||||
>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" class="mr-2" @click="updateUser">
|
||||
<v-icon left> mdi-content-save </v-icon>
|
||||
{{ $t("general.save") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" sm="12">
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
Reset Password
|
||||
<v-spacer></v-spacer>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
v-model="password.current"
|
||||
light="light"
|
||||
prepend-icon="mdi-lock"
|
||||
label="Current Password"
|
||||
:type="showPassword.current ? 'text' : 'password'"
|
||||
:append-icon="showPassword.current ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword.current = !showPassword.current"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="password.newOne"
|
||||
light="light"
|
||||
prepend-icon="mdi-lock"
|
||||
label="New Password"
|
||||
:type="showPassword.newOne ? 'text' : 'password'"
|
||||
:append-icon="showPassword.newOne ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword.newOne = !showPassword.newOne"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="password.newTwo"
|
||||
light="light"
|
||||
prepend-icon="mdi-lock"
|
||||
label="Confirm Password"
|
||||
:type="showPassword.newTwo ? 'text' : 'password'"
|
||||
:append-icon="showPassword.newTwo ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword.newTwo = !showPassword.newTwo"
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="accent" class="mr-2" @click="changePassword">
|
||||
<v-icon left> mdi-lock </v-icon>
|
||||
{{ $t("settings.change-password") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -54,6 +102,16 @@ export default {
|
|||
pageTitle: "My Profile",
|
||||
data() {
|
||||
return {
|
||||
password: {
|
||||
current: "",
|
||||
newOne: "",
|
||||
newTwo: "",
|
||||
},
|
||||
showPassword: {
|
||||
current: false,
|
||||
newOne: false,
|
||||
newTwo: false,
|
||||
},
|
||||
loading: false,
|
||||
user: {
|
||||
fullName: "Change Me",
|
||||
|
@ -87,6 +145,14 @@ export default {
|
|||
this.refreshProfile();
|
||||
this.loading = false;
|
||||
},
|
||||
async changePassword() {
|
||||
let data = {
|
||||
currentPassword: this.password.current,
|
||||
newPassword: this.password.newOne,
|
||||
};
|
||||
|
||||
await api.users.changePassword(this.user.id, data);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -4,15 +4,70 @@
|
|||
<v-slide-x-transition hide-on-leave>
|
||||
<router-view></router-view>
|
||||
</v-slide-x-transition>
|
||||
<!-- <v-footer fixed>
|
||||
<v-col class="text-center" cols="12">
|
||||
{{ $t("settings.current") }}
|
||||
{{ version }} |
|
||||
{{ $t("settings.latest") }}
|
||||
{{ latestVersion }}
|
||||
·
|
||||
<a href="https://hay-kot.github.io/mealie/" target="_blank">
|
||||
{{ $t("settings.explore-the-docs") }}
|
||||
</a>
|
||||
·
|
||||
<a
|
||||
href="https://hay-kot.github.io/mealie/contributors/non-coders/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t("settings.contribute") }}
|
||||
</a>
|
||||
</v-col>
|
||||
</v-footer> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AdminSidebar from "../../components/Admin/AdminSidebar";
|
||||
import AdminSidebar from "@/components/Admin/AdminSidebar";
|
||||
import axios from "axios";
|
||||
import api from "@/api";
|
||||
export default {
|
||||
components: {
|
||||
AdminSidebar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
latestVersion: null,
|
||||
version: null,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.getVersion();
|
||||
let versionData = await api.meta.get_version();
|
||||
this.version = versionData.version;
|
||||
},
|
||||
computed: {
|
||||
newVersion() {
|
||||
if ((this.latestVersion != null) & (this.latestVersion != this.version)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getVersion() {
|
||||
let response = await axios.get(
|
||||
"https://api.github.com/repos/hay-kot/mealie/releases/latest",
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
Authorization: null,
|
||||
},
|
||||
}
|
||||
);
|
||||
this.latestVersion = response.data.tag_name;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ const state = {
|
|||
isDark: false,
|
||||
isLoggedIn: false,
|
||||
token: "",
|
||||
userData: {},
|
||||
};
|
||||
|
||||
const mutations = {
|
||||
|
@ -46,6 +47,10 @@ const mutations = {
|
|||
axios.defaults.headers.common["Authorization"] = `Bearer ${payload}`;
|
||||
state.token = payload;
|
||||
},
|
||||
|
||||
setUserData(state, payload) {
|
||||
state.userData = payload;
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
|
@ -58,7 +63,6 @@ const actions = {
|
|||
}
|
||||
},
|
||||
|
||||
|
||||
async initTheme({ dispatch, getters }) {
|
||||
//If theme is empty resetTheme
|
||||
if (Object.keys(getters.getActiveTheme).length === 0) {
|
||||
|
@ -77,6 +81,7 @@ const getters = {
|
|||
getIsDark: state => state.isDark,
|
||||
getIsLoggedIn: state => state.isLoggedIn,
|
||||
getToken: state => state.token,
|
||||
getUserData: state => state.userData,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -46,6 +46,7 @@ NEXTCLOUD_DIR = MIGRATION_DIR.joinpath("nextcloud")
|
|||
CHOWDOWN_DIR = MIGRATION_DIR.joinpath("chowdown")
|
||||
TEMPLATE_DIR = DATA_DIR.joinpath("templates")
|
||||
SQLITE_DIR = DATA_DIR.joinpath("db")
|
||||
RECIPE_DATA_DIR = DATA_DIR.joinpath("recipes")
|
||||
TEMP_DIR = DATA_DIR.joinpath(".temp")
|
||||
|
||||
REQUIRED_DIRS = [
|
||||
|
@ -58,6 +59,7 @@ REQUIRED_DIRS = [
|
|||
SQLITE_DIR,
|
||||
NEXTCLOUD_DIR,
|
||||
CHOWDOWN_DIR,
|
||||
RECIPE_DATA_DIR
|
||||
]
|
||||
|
||||
ensure_dirs()
|
||||
|
|
|
@ -61,6 +61,15 @@ class _Users(BaseDocument):
|
|||
self.primary_key = "id"
|
||||
self.sql_model = User
|
||||
|
||||
def update_password(self, session, id, password: str):
|
||||
entry = self._query_one(session=session, match_value=id)
|
||||
entry.update_password(password)
|
||||
return_data = entry.dict()
|
||||
session.commit()
|
||||
|
||||
return return_data
|
||||
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self) -> None:
|
||||
|
|
|
@ -42,3 +42,6 @@ class User(SqlAlchemyBase, BaseMixins):
|
|||
self.email = email
|
||||
self.family = family
|
||||
self.admin = admin
|
||||
|
||||
def update_password(self, password):
|
||||
self.password = password
|
||||
|
|
|
@ -8,6 +8,7 @@ from fastapi_login.exceptions import InvalidCredentialsException
|
|||
from routes.deps import manager, query_user
|
||||
from schema.user import UserInDB
|
||||
from sqlalchemy.orm.session import Session
|
||||
from schema.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["Auth"])
|
||||
|
||||
|
@ -29,4 +30,4 @@ def token(
|
|||
access_token = manager.create_access_token(
|
||||
data=dict(sub=email), expires=timedelta(hours=2)
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
return SnackResponse.success("User Successfully Logged In", {"access_token": access_token, "token_type": "bearer"})
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from core.security import get_password_hash
|
||||
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 schema.user import UserBase, UserIn, UserInDB, UserOut
|
||||
from schema.snackbar import SnackResponse
|
||||
from schema.user import ChangePassword, UserBase, UserIn, UserInDB, UserOut
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["Users"])
|
||||
|
@ -17,12 +18,11 @@ async def create_user(
|
|||
current_user=Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Returns a list of all user in the Database """
|
||||
|
||||
new_user.password = get_password_hash(new_user.password)
|
||||
|
||||
data = db.users.create(session, new_user.dict())
|
||||
return data
|
||||
return SnackResponse.success(f"User Created: {new_user.full_name}", data)
|
||||
|
||||
|
||||
@router.get("", response_model=list[UserOut])
|
||||
|
@ -38,7 +38,7 @@ async def get_all_users(
|
|||
|
||||
|
||||
@router.get("/self", response_model=UserOut)
|
||||
async def get_user_by_id(
|
||||
async def get_logged_in_user(
|
||||
current_user: UserInDB = Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
|
@ -69,8 +69,30 @@ async def update_user(
|
|||
access_token = manager.create_access_token(
|
||||
data=dict(sub=email), expires=timedelta(hours=2)
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
return
|
||||
access_token = {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
return SnackResponse.success("User Updated", access_token)
|
||||
|
||||
|
||||
@router.put("/{id}/password")
|
||||
async def update_password(
|
||||
id: int,
|
||||
password_change: ChangePassword,
|
||||
current_user: UserInDB = Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Resets the User Password"""
|
||||
|
||||
match_passwords = verify_password(
|
||||
password_change.current_password, current_user.password
|
||||
)
|
||||
match_id = current_user.id == id
|
||||
|
||||
if match_passwords and match_id:
|
||||
new_password = get_password_hash(password_change.new_password)
|
||||
db.users.update_password(session, id, new_password)
|
||||
|
||||
return SnackResponse.success("Password Updated")
|
||||
|
||||
|
||||
@router.delete("/{id}")
|
||||
|
@ -81,5 +103,9 @@ async def delete_user(
|
|||
):
|
||||
""" Removes a user from the database. Must be the current user or a super user"""
|
||||
|
||||
if id == 1:
|
||||
return SnackResponse.error("Error! Cannot Delete Super User")
|
||||
|
||||
if current_user.id == id or current_user.admin:
|
||||
return db.users.delete(session, id)
|
||||
db.users.delete(session, id)
|
||||
return SnackResponse.error(f"User Deleted")
|
||||
|
|
|
@ -5,6 +5,11 @@ from fastapi_camelcase import CamelModel
|
|||
# from pydantic import EmailStr
|
||||
|
||||
|
||||
class ChangePassword(CamelModel):
|
||||
current_password: str
|
||||
new_password: str
|
||||
|
||||
|
||||
class UserBase(CamelModel):
|
||||
full_name: Optional[str] = None
|
||||
email: str
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue