super user CRUD

This commit is contained in:
hay-kot 2021-02-23 14:57:01 -09:00
commit 60806b783e
8 changed files with 170 additions and 59 deletions

View file

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

View file

@ -1,14 +1,17 @@
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) {
@ -17,7 +20,30 @@ export default {
"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;
},
}; };

View file

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

View file

@ -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,13 +52,19 @@
v-model="editedItem.family" v-model="editedItem.family"
label="Family Group" label="Family Group"
></v-text-field> ></v-text-field>
</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-col cols="12" sm="12" md="3">
<v-switch <v-switch
v-model="editedItem.admin" v-model="editedItem.admin"
label="Admin" label="Admin"
></v-switch> ></v-switch>
</v-col> </v-col>
</v-col>
</v-row> </v-row>
</v-container> </v-container>
</v-card-text> </v-card-text>
@ -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",
email: "changeme@email.com",
family: "public",
admin: false,
}, },
];
async deleteUser() {
await api.users.delete(this.editedIndex);
this.initialize();
}, },
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();
}, },
}, },

View file

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

View file

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

View file

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

View file

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