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