password reset

This commit is contained in:
hay-kot 2021-02-23 19:18:32 -09:00
commit 7024861397
14 changed files with 257 additions and 67 deletions

View file

@ -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;

View file

@ -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) {

View file

@ -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

View file

@ -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;
},
},

View file

@ -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);

View file

@ -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>

View file

@ -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>

View file

@ -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 {

View file

@ -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()

View file

@ -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:

View file

@ -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

View file

@ -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"})

View file

@ -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")

View file

@ -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