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}`, users: `${userPrefix}`,
self: `${userPrefix}/self`, self: `${userPrefix}/self`,
userID: id => `${userPrefix}/${id}`, userID: id => `${userPrefix}/${id}`,
password: id => `${userPrefix}/${id}/password`,
}; };
export default { export default {
@ -42,6 +43,10 @@ export default {
let response = await apiReq.put(usersURLs.userID(user.id), user); let response = await apiReq.put(usersURLs.userID(user.id), user);
return response.data; return response.data;
}, },
async changePassword(id, password) {
let response = await apiReq.put(usersURLs.password(id), password);
return response.data;
},
async delete(id) { async delete(id) {
let response = await apiReq.delete(usersURLs.userID(id)); let response = await apiReq.delete(usersURLs.userID(id));
return response.data; return response.data;

View file

@ -27,8 +27,10 @@
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>
<v-list-item-title>Jane Smith</v-list-item-title> <v-list-item-title> {{ user.fullName }}</v-list-item-title>
<v-list-item-subtitle>Admin</v-list-item-subtitle> <v-list-item-subtitle>
{{ user.admin ? "Admin" : "User" }}</v-list-item-subtitle
>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
</template> </template>
@ -50,7 +52,7 @@
</v-list> </v-list>
<v-divider></v-divider> <v-divider></v-divider>
<v-list nav dense> <v-list nav dense v-if="user.admin">
<v-list-item <v-list-item
v-for="nav in superLinks" v-for="nav in superLinks"
:key="nav.title" :key="nav.title"
@ -115,11 +117,17 @@ export default {
], ],
}; };
}, },
mounted() { async mounted() {
this.mobile = this.viewScale(); this.mobile = this.viewScale();
this.showSidebar = !this.viewScale(); this.showSidebar = !this.viewScale();
}, },
computed: {
user() {
return this.$store.getters.getUserData;
},
},
methods: { methods: {
viewScale() { viewScale() {
switch (this.$vuetify.breakpoint.name) { switch (this.$vuetify.breakpoint.name) {

View file

@ -1,10 +1,13 @@
<template> <template>
<v-card flat> <v-card flat>
<v-card-text> <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-row align="center" justify="center" dense class="mb-n7 pb-n5">
<v-col cols="12" sm="3" md="2"> <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>
<v-col cols="12" sm="5" md="5"> <v-col cols="12" sm="5" md="5">
<v-slider <v-slider

View file

@ -133,8 +133,11 @@ export default {
this.$emit("logged-in"); this.$emit("logged-in");
this.clear(); 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; this.loading = false;
}, },
}, },

View file

@ -186,7 +186,7 @@ export default {
}, },
async deleteUser() { async deleteUser() {
await api.users.delete(this.editedIndex); await api.users.delete(this.activeId);
this.initialize(); this.initialize();
}, },
@ -227,7 +227,6 @@ export default {
async save() { async save() {
if (this.editedIndex > -1) { if (this.editedIndex > -1) {
console.log("New User", this.editedItem);
api.users.update(this.editedItem); api.users.update(this.editedItem);
} else { } else {
api.users.create(this.editedItem); api.users.create(this.editedItem);

View file

@ -1,50 +1,98 @@
<template> <template>
<v-card> <v-row dense>
<v-card-title class="headline"> <v-col cols="12" md="12" sm="12">
<span> <v-card>
<v-avatar color="accent" size="40" class="mr-2" v-if="!loading"> <v-card-title class="headline">
<img src="https://cdn.vuetifyjs.com/images/john.jpg" alt="John" /> <span>
</v-avatar> <v-avatar color="accent" size="40" class="mr-2" v-if="!loading">
<v-progress-circular <img src="https://cdn.vuetifyjs.com/images/john.jpg" alt="John" />
v-else </v-avatar>
indeterminate <v-progress-circular
color="primary" v-else
large indeterminate
class="mr-2" color="primary"
> large
</v-progress-circular> class="mr-2"
</span> >
Profile </v-progress-circular>
<v-spacer></v-spacer> </span>
User ID: {{ user.id }} Profile
</v-card-title> <v-spacer></v-spacer>
<v-divider></v-divider> User ID: {{ user.id }}
<v-card-text> </v-card-title>
<v-form> <v-divider></v-divider>
<v-text-field label="Full Name" v-model="user.fullName"> </v-text-field> <v-card-text>
<v-text-field label="Email" v-model="user.email"> </v-text-field> <v-form>
<v-text-field <v-text-field label="Full Name" v-model="user.fullName">
label="Family" </v-text-field>
readonly <v-text-field label="Email" v-model="user.email"> </v-text-field>
v-model="user.family" <v-text-field
persistent-hint label="Family"
hint="Family groups can only be set by administrators" readonly
> v-model="user.family"
</v-text-field> persistent-hint
</v-form> hint="Family groups can only be set by administrators"
</v-card-text> >
<v-card-actions> </v-text-field>
<v-btn color="accent" class="mr-2"> </v-form>
<v-icon left> mdi-lock </v-icon> </v-card-text>
{{ $t("settings.change-password") }} <v-card-actions>
</v-btn> <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"> <v-icon left> mdi-content-save </v-icon>
<v-icon left> mdi-content-save </v-icon> {{ $t("general.save") }}
{{ $t("general.save") }} </v-btn>
</v-btn> </v-card-actions>
</v-card-actions> </v-card>
</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> </template>
<script> <script>
@ -54,6 +102,16 @@ export default {
pageTitle: "My Profile", pageTitle: "My Profile",
data() { data() {
return { return {
password: {
current: "",
newOne: "",
newTwo: "",
},
showPassword: {
current: false,
newOne: false,
newTwo: false,
},
loading: false, loading: false,
user: { user: {
fullName: "Change Me", fullName: "Change Me",
@ -87,6 +145,14 @@ export default {
this.refreshProfile(); this.refreshProfile();
this.loading = false; this.loading = false;
}, },
async changePassword() {
let data = {
currentPassword: this.password.current,
newPassword: this.password.newOne,
};
await api.users.changePassword(this.user.id, data);
},
}, },
}; };
</script> </script>

View file

@ -4,15 +4,70 @@
<v-slide-x-transition hide-on-leave> <v-slide-x-transition hide-on-leave>
<router-view></router-view> <router-view></router-view>
</v-slide-x-transition> </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> </div>
</template> </template>
<script> <script>
import AdminSidebar from "../../components/Admin/AdminSidebar"; import AdminSidebar from "@/components/Admin/AdminSidebar";
import axios from "axios";
import api from "@/api";
export default { export default {
components: { components: {
AdminSidebar, 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> </script>

View file

@ -21,6 +21,7 @@ const state = {
isDark: false, isDark: false,
isLoggedIn: false, isLoggedIn: false,
token: "", token: "",
userData: {},
}; };
const mutations = { const mutations = {
@ -46,6 +47,10 @@ const mutations = {
axios.defaults.headers.common["Authorization"] = `Bearer ${payload}`; axios.defaults.headers.common["Authorization"] = `Bearer ${payload}`;
state.token = payload; state.token = payload;
}, },
setUserData(state, payload) {
state.userData = payload;
},
}; };
const actions = { const actions = {
@ -58,7 +63,6 @@ const actions = {
} }
}, },
async initTheme({ dispatch, getters }) { async initTheme({ dispatch, getters }) {
//If theme is empty resetTheme //If theme is empty resetTheme
if (Object.keys(getters.getActiveTheme).length === 0) { if (Object.keys(getters.getActiveTheme).length === 0) {
@ -77,6 +81,7 @@ const getters = {
getIsDark: state => state.isDark, getIsDark: state => state.isDark,
getIsLoggedIn: state => state.isLoggedIn, getIsLoggedIn: state => state.isLoggedIn,
getToken: state => state.token, getToken: state => state.token,
getUserData: state => state.userData,
}; };
export default { export default {

View file

@ -46,6 +46,7 @@ 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")
SQLITE_DIR = DATA_DIR.joinpath("db") SQLITE_DIR = DATA_DIR.joinpath("db")
RECIPE_DATA_DIR = DATA_DIR.joinpath("recipes")
TEMP_DIR = DATA_DIR.joinpath(".temp") TEMP_DIR = DATA_DIR.joinpath(".temp")
REQUIRED_DIRS = [ REQUIRED_DIRS = [
@ -58,6 +59,7 @@ REQUIRED_DIRS = [
SQLITE_DIR, SQLITE_DIR,
NEXTCLOUD_DIR, NEXTCLOUD_DIR,
CHOWDOWN_DIR, CHOWDOWN_DIR,
RECIPE_DATA_DIR
] ]
ensure_dirs() ensure_dirs()

View file

@ -61,6 +61,15 @@ class _Users(BaseDocument):
self.primary_key = "id" self.primary_key = "id"
self.sql_model = User 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: class Database:
def __init__(self) -> None: def __init__(self) -> None:

View file

@ -42,3 +42,6 @@ class User(SqlAlchemyBase, BaseMixins):
self.email = email self.email = email
self.family = family self.family = family
self.admin = admin 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 routes.deps import manager, query_user
from schema.user import UserInDB from schema.user import UserInDB
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from schema.snackbar import SnackResponse
router = APIRouter(prefix="/api/auth", tags=["Auth"]) router = APIRouter(prefix="/api/auth", tags=["Auth"])
@ -29,4 +30,4 @@ def token(
access_token = manager.create_access_token( access_token = manager.create_access_token(
data=dict(sub=email), expires=timedelta(hours=2) 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 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.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
from routes.deps import manager, query_user 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 from sqlalchemy.orm.session import Session
router = APIRouter(prefix="/api/users", tags=["Users"]) router = APIRouter(prefix="/api/users", tags=["Users"])
@ -17,12 +18,11 @@ async def create_user(
current_user=Depends(manager), current_user=Depends(manager),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
""" Returns a list of all user in the Database """
new_user.password = get_password_hash(new_user.password) new_user.password = get_password_hash(new_user.password)
data = db.users.create(session, new_user.dict()) 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]) @router.get("", response_model=list[UserOut])
@ -38,7 +38,7 @@ async def get_all_users(
@router.get("/self", response_model=UserOut) @router.get("/self", response_model=UserOut)
async def get_user_by_id( async def get_logged_in_user(
current_user: UserInDB = Depends(manager), current_user: UserInDB = Depends(manager),
session: Session = Depends(generate_session), session: Session = Depends(generate_session),
): ):
@ -69,8 +69,30 @@ async def update_user(
access_token = manager.create_access_token( access_token = manager.create_access_token(
data=dict(sub=email), expires=timedelta(hours=2) data=dict(sub=email), expires=timedelta(hours=2)
) )
return {"access_token": access_token, "token_type": "bearer"} access_token = {"access_token": access_token, "token_type": "bearer"}
return
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}") @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""" """ 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: 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 # from pydantic import EmailStr
class ChangePassword(CamelModel):
current_password: str
new_password: str
class UserBase(CamelModel): class UserBase(CamelModel):
full_name: Optional[str] = None full_name: Optional[str] = None
email: str email: str