mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
super user CRUD
This commit is contained in:
parent
6cb8e5cc29
commit
60806b783e
8 changed files with 170 additions and 59 deletions
|
@ -1,6 +1,11 @@
|
|||
const baseURL = "/api/";
|
||||
import axios from "axios";
|
||||
import utils from "@/utils";
|
||||
import { store } from "../store/store";
|
||||
|
||||
axios.defaults.headers.common[
|
||||
"Authorization"
|
||||
] = `Bearer ${store.getters.getToken}`;
|
||||
|
||||
function processResponse(response) {
|
||||
try {
|
||||
|
|
|
@ -1,23 +1,49 @@
|
|||
import { baseURL } from "./api-utils";
|
||||
import { apiReq } from "./api-utils";
|
||||
const authPrefix = baseURL + "auth/";
|
||||
const authPrefix = baseURL + "auth";
|
||||
const userPrefix = baseURL + "users";
|
||||
|
||||
const authURLs = {
|
||||
token: `${authPrefix}token`,
|
||||
token: `${authPrefix}/token`,
|
||||
};
|
||||
|
||||
// const usersURLs = {
|
||||
|
||||
// }
|
||||
const usersURLs = {
|
||||
users: `${userPrefix}`,
|
||||
self: `${userPrefix}/self`,
|
||||
userID: id => `${userPrefix}/${id}`,
|
||||
};
|
||||
|
||||
export default {
|
||||
async login(formData) {
|
||||
let response = await apiReq.post(authURLs.token, formData, {
|
||||
let response = await apiReq.post(authURLs.token, formData, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
});
|
||||
console.log(response);
|
||||
return response;
|
||||
},
|
||||
async allUsers() {
|
||||
let response = await apiReq.get(usersURLs.users);
|
||||
return response.data;
|
||||
},
|
||||
async create(user) {
|
||||
let response = await apiReq.post(usersURLs.users, user);
|
||||
return response.data;
|
||||
},
|
||||
async self() {
|
||||
let response = await apiReq.get(usersURLs.self);
|
||||
return response.data;
|
||||
},
|
||||
async byID(id) {
|
||||
let response = await apiReq.get(usersURLs.userID(id));
|
||||
return response.data;
|
||||
},
|
||||
async update(user) {
|
||||
let response = await apiReq.put(usersURLs.userID(user.id), user);
|
||||
return response.data;
|
||||
},
|
||||
async delete(id) {
|
||||
let response = await apiReq.delete(usersURLs.userID(id));
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -21,14 +21,20 @@
|
|||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="cancel"> {{ $t("general.cancel") }} </v-btn>
|
||||
<v-btn :color="color" text @click="confirm"> {{ $t("general.confirm") }} </v-btn>
|
||||
<v-btn color="grey" text @click="cancel">
|
||||
{{ $t("general.cancel") }}
|
||||
</v-btn>
|
||||
<v-btn :color="color" text @click="confirm">
|
||||
{{ $t("general.confirm") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const CLOSE_EVENT = "close";
|
||||
const OPEN_EVENT = "open";
|
||||
/**
|
||||
* Confirmation Component used to add a second validaion step to an action.
|
||||
* @version 1.0.1
|
||||
|
@ -51,7 +57,7 @@ export default {
|
|||
*/
|
||||
icon: {
|
||||
type: String,
|
||||
default: "mid-alert-circle"
|
||||
default: "mid-alert-circle",
|
||||
},
|
||||
/**
|
||||
* Color theme of the component. Chose one of the defined theme colors.
|
||||
|
@ -59,28 +65,35 @@ export default {
|
|||
*/
|
||||
color: {
|
||||
type: String,
|
||||
default: "error"
|
||||
default: "error",
|
||||
},
|
||||
/**
|
||||
* Define the max width of the component.
|
||||
*/
|
||||
width: {
|
||||
type: Number,
|
||||
default: 400
|
||||
default: 400,
|
||||
},
|
||||
/**
|
||||
* zIndex of the component.
|
||||
*/
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 200
|
||||
}
|
||||
default: 200,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
dialog() {
|
||||
if (this.dialog === false) {
|
||||
this.$emit(CLOSE_EVENT);
|
||||
} else this.$emit(OPEN_EVENT);
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
/**
|
||||
* Keep state of open or closed
|
||||
*/
|
||||
dialog: false
|
||||
dialog: false,
|
||||
}),
|
||||
methods: {
|
||||
/**
|
||||
|
@ -120,8 +133,8 @@ export default {
|
|||
|
||||
//Hide Modal
|
||||
this.dialog = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<v-row>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.full_name"
|
||||
v-model="editedItem.fullName"
|
||||
label="Full Name"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
@ -52,12 +52,18 @@
|
|||
v-model="editedItem.family"
|
||||
label="Family Group"
|
||||
></v-text-field>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-switch
|
||||
v-model="editedItem.admin"
|
||||
label="Admin"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6" v-if="showPassword">
|
||||
<v-text-field
|
||||
v-model="editedItem.password"
|
||||
label="User Password"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="3">
|
||||
<v-switch
|
||||
v-model="editedItem.admin"
|
||||
label="Admin"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
@ -76,10 +82,14 @@
|
|||
</v-dialog>
|
||||
<Confirmation
|
||||
ref="deleteUserDialog"
|
||||
title="Confirm Delete User"
|
||||
message="Are you sure you want to delete the user?"
|
||||
title="Confirm User Deletion"
|
||||
:message="
|
||||
`Are you sure you want to delete the user <b>${activeName} ID: ${activeId}<b/>`
|
||||
"
|
||||
icon="mdi-alert"
|
||||
@confirm="deleteItemConfirm"
|
||||
@confirm="deleteUser"
|
||||
:width="450"
|
||||
@close="closeDelete"
|
||||
/>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
@ -110,11 +120,13 @@
|
|||
|
||||
<script>
|
||||
import Confirmation from "@/components/UI/Confirmation";
|
||||
import api from "@/api";
|
||||
export default {
|
||||
components: { Confirmation },
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
dialogDelete: false,
|
||||
activeId: null,
|
||||
activeName: null,
|
||||
headers: [
|
||||
{
|
||||
text: "User ID",
|
||||
|
@ -122,7 +134,7 @@ export default {
|
|||
sortable: false,
|
||||
value: "id",
|
||||
},
|
||||
{ text: "Full Name", value: "full_name" },
|
||||
{ text: "Full Name", value: "fullName" },
|
||||
{ text: "Email", value: "email" },
|
||||
{ text: "Family", value: "family" },
|
||||
{ text: "Admin", value: "admin" },
|
||||
|
@ -132,14 +144,14 @@ export default {
|
|||
editedIndex: -1,
|
||||
editedItem: {
|
||||
id: 0,
|
||||
full_name: "",
|
||||
fullName: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
},
|
||||
defaultItem: {
|
||||
id: 0,
|
||||
full_name: "",
|
||||
fullName: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
|
@ -150,6 +162,9 @@ export default {
|
|||
formTitle() {
|
||||
return this.editedIndex === -1 ? "New User" : "Edit User";
|
||||
},
|
||||
showPassword() {
|
||||
return this.editedIndex === -1 ? true : false;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
@ -166,16 +181,13 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
initialize() {
|
||||
this.users = [
|
||||
{
|
||||
id: 1,
|
||||
full_name: "Change Me",
|
||||
email: "changeme@email.com",
|
||||
family: "public",
|
||||
admin: false,
|
||||
},
|
||||
];
|
||||
async initialize() {
|
||||
this.users = await api.users.allUsers();
|
||||
},
|
||||
|
||||
async deleteUser() {
|
||||
await api.users.delete(this.editedIndex);
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
editItem(item) {
|
||||
|
@ -185,6 +197,8 @@ export default {
|
|||
},
|
||||
|
||||
deleteItem(item) {
|
||||
this.activeId = item.id;
|
||||
this.activeName = item.fullName;
|
||||
this.editedIndex = this.users.indexOf(item);
|
||||
this.editedItem = Object.assign({}, item);
|
||||
this.$refs.deleteUserDialog.open();
|
||||
|
@ -211,12 +225,14 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
save() {
|
||||
async save() {
|
||||
if (this.editedIndex > -1) {
|
||||
Object.assign(this.users[this.editedIndex], this.editedItem);
|
||||
console.log("New User", this.editedItem);
|
||||
api.users.update(this.editedItem);
|
||||
} else {
|
||||
this.users.push(this.editedItem);
|
||||
api.users.create(this.editedItem);
|
||||
}
|
||||
await this.initialize();
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
|
|
|
@ -2,18 +2,35 @@
|
|||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
<span>
|
||||
<v-avatar color="accent" size="40" class="mr-2">
|
||||
<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-text-field>
|
||||
<v-text-field label="Email"> </v-text-field>
|
||||
<v-text-field label="Group" readonly> </v-text-field>
|
||||
<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>
|
||||
|
@ -22,7 +39,7 @@
|
|||
{{ $t("settings.change-password") }}
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" class="mr-2">
|
||||
<v-btn color="success" class="mr-2" @click="updateUser">
|
||||
<v-icon left> mdi-content-save </v-icon>
|
||||
{{ $t("general.save") }}
|
||||
</v-btn>
|
||||
|
@ -32,27 +49,43 @@
|
|||
|
||||
<script>
|
||||
// import AvatarPicker from '@/components/AvatarPicker'
|
||||
import api from "@/api";
|
||||
export default {
|
||||
pageTitle: "My Profile",
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
form: {
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
contactEmail: "john@doe.com",
|
||||
avatar: "MALE_CAUCASIAN_BLOND_BEARD",
|
||||
user: {
|
||||
fullName: "Change Me",
|
||||
email: "changeme@email.com",
|
||||
family: "public",
|
||||
admin: true,
|
||||
id: 1,
|
||||
},
|
||||
showAvatarPicker: false,
|
||||
};
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.refreshProfile();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async refreshProfile() {
|
||||
this.user = await api.users.self();
|
||||
},
|
||||
openAvatarPicker() {
|
||||
this.showAvatarPicker = true;
|
||||
},
|
||||
selectAvatar(avatar) {
|
||||
this.form.avatar = avatar;
|
||||
this.user.avatar = avatar;
|
||||
},
|
||||
async updateUser() {
|
||||
this.loading = true;
|
||||
let newKey = await api.users.update(this.user);
|
||||
this.$store.commit("setToken", newKey.access_token);
|
||||
this.refreshProfile();
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import api from "@/api";
|
||||
import Vuetify from "../../plugins/vuetify";
|
||||
import axios from "axios";
|
||||
|
||||
function inDarkMode(payload) {
|
||||
let isDark;
|
||||
|
@ -42,6 +43,7 @@ const mutations = {
|
|||
},
|
||||
setToken(state, payload) {
|
||||
state.isLoggedIn = true;
|
||||
axios.defaults.headers.common["Authorization"] = `Bearer ${payload}`;
|
||||
state.token = payload;
|
||||
},
|
||||
};
|
||||
|
@ -56,6 +58,7 @@ const actions = {
|
|||
}
|
||||
},
|
||||
|
||||
|
||||
async initTheme({ dispatch, getters }) {
|
||||
//If theme is empty resetTheme
|
||||
if (Object.keys(getters.getActiveTheme).length === 0) {
|
||||
|
|
|
@ -3,7 +3,6 @@ from fastapi.logger import logger
|
|||
from schema.settings import SiteSettings, Webhooks
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm.session import Session
|
||||
from fastapi.logger import logger
|
||||
|
||||
from db.database import db
|
||||
from db.db_setup import create_session
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from datetime import timedelta
|
||||
|
||||
from core.security import get_password_hash
|
||||
from db.database import db
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends
|
||||
from routes.deps import manager
|
||||
from routes.deps import manager, query_user
|
||||
from schema.user import UserBase, UserIn, UserInDB, UserOut
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
|
@ -35,6 +37,14 @@ async def get_all_users(
|
|||
return {"details": "user not authorized"}
|
||||
|
||||
|
||||
@router.get("/self", response_model=UserOut)
|
||||
async def get_user_by_id(
|
||||
current_user: UserInDB = Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
return current_user.dict()
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=UserOut)
|
||||
async def get_user_by_id(
|
||||
id: int,
|
||||
|
@ -44,7 +54,7 @@ async def get_user_by_id(
|
|||
return db.users.get(session, id)
|
||||
|
||||
|
||||
@router.put("/{id}", response_model=UserOut)
|
||||
@router.put("/{id}")
|
||||
async def update_user(
|
||||
id: int,
|
||||
new_data: UserBase,
|
||||
|
@ -53,7 +63,13 @@ async def update_user(
|
|||
):
|
||||
|
||||
if current_user.id == id or current_user.admin:
|
||||
return db.users.update(session, id, new_data.dict())
|
||||
updated_user = db.users.update(session, id, new_data.dict())
|
||||
email = updated_user.get("email")
|
||||
if current_user.id == id:
|
||||
access_token = manager.create_access_token(
|
||||
data=dict(sub=email), expires=timedelta(hours=2)
|
||||
)
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
return
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue