mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 06:23:34 -07:00
signups
This commit is contained in:
parent
eea356e5c1
commit
f22522a52c
15 changed files with 934 additions and 242 deletions
|
@ -86,6 +86,8 @@ export default {
|
|||
search: false,
|
||||
}),
|
||||
methods: {
|
||||
// For Later!
|
||||
|
||||
/**
|
||||
* Checks if 'system' is set for dark mode and then sets the corrisponding value for vuetify
|
||||
*/
|
||||
|
|
|
@ -7,8 +7,8 @@ import migration from "./migration";
|
|||
import myUtils from "./upload";
|
||||
import category from "./category";
|
||||
import meta from "./meta";
|
||||
import users from "./users"
|
||||
|
||||
import users from "./users";
|
||||
import signUps from "./signUps";
|
||||
|
||||
export default {
|
||||
recipes: recipe,
|
||||
|
@ -20,5 +20,6 @@ export default {
|
|||
utils: myUtils,
|
||||
categories: category,
|
||||
meta: meta,
|
||||
users: users
|
||||
users: users,
|
||||
signUps: signUps,
|
||||
};
|
||||
|
|
30
frontend/src/api/signUps.js
Normal file
30
frontend/src/api/signUps.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { baseURL } from "./api-utils";
|
||||
import { apiReq } from "./api-utils";
|
||||
|
||||
const signUpPrefix = baseURL + "users/sign-ups";
|
||||
|
||||
const signUpURLs = {
|
||||
all: `${signUpPrefix}`,
|
||||
createToken: `${signUpPrefix}`,
|
||||
deleteToken: token => `${signUpPrefix}/${token}`,
|
||||
createUser: token => `${signUpPrefix}/${token}`,
|
||||
};
|
||||
|
||||
export default {
|
||||
async getAll() {
|
||||
let response = await apiReq.get(signUpURLs.all);
|
||||
return response.data;
|
||||
},
|
||||
async createToken(data) {
|
||||
let response = await apiReq.post(signUpURLs.createToken, data);
|
||||
return response.data;
|
||||
},
|
||||
async deleteToken(token) {
|
||||
let response = await apiReq.delete(signUpURLs.deleteToken(token));
|
||||
return response.data;
|
||||
},
|
||||
async createUser(token, data) {
|
||||
let response = await apiReq.post(signUpURLs.createUser(token), data);
|
||||
return response.data;
|
||||
},
|
||||
};
|
|
@ -7,6 +7,7 @@ const authURLs = {
|
|||
token: `${authPrefix}/token`,
|
||||
};
|
||||
|
||||
|
||||
const usersURLs = {
|
||||
users: `${userPrefix}`,
|
||||
self: `${userPrefix}/self`,
|
||||
|
|
252
frontend/src/components/Admin/ManageUsers/TheGroupTable.vue
Normal file
252
frontend/src/components/Admin/ManageUsers/TheGroupTable.vue
Normal file
|
@ -0,0 +1,252 @@
|
|||
<template>
|
||||
<v-card outlined>
|
||||
<Confirmation
|
||||
ref="deleteUserDialog"
|
||||
title="Confirm User Deletion"
|
||||
:message="
|
||||
`Are you sure you want to delete the user <b>${activeName} ID: ${activeId}<b/>`
|
||||
"
|
||||
icon="mdi-alert"
|
||||
@confirm="deleteUser"
|
||||
:width="450"
|
||||
@close="closeDelete"
|
||||
/>
|
||||
<v-toolbar flat>
|
||||
<v-icon large color="accent" class="mr-1">
|
||||
mdi-account-group
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headine">
|
||||
User Groups
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer> </v-spacer>
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn small color="success" dark v-bind="attrs" v-on="on">
|
||||
Create Group
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-app-bar dark dense color="primary">
|
||||
<v-icon left>
|
||||
mdi-account
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
{{ formTitle }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-title class="headline">
|
||||
User ID: {{ editedItem.id }}
|
||||
</v-toolbar-title>
|
||||
</v-app-bar>
|
||||
|
||||
<v-card-text>
|
||||
<v-form ref="newUser">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.fullName"
|
||||
label="Full Name"
|
||||
:rules="[existsRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.email"
|
||||
label="Email"
|
||||
:rules="[existsRule, emailRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.family"
|
||||
label="Family Group"
|
||||
></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"
|
||||
:rules="[existsRule, minRule]"
|
||||
></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-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="close">
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="save">
|
||||
Save
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-toolbar>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-data-table :headers="headers" :items="users" sort-by="calories">
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn class="mr-1" small color="error" @click="deleteItem(item)">
|
||||
<v-icon small left>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
Delete
|
||||
</v-btn>
|
||||
<v-btn small color="success" @click="editItem(item)">
|
||||
<v-icon small left class="mr-2">
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
Edit
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:item.admin="{ item }">
|
||||
{{ item.admin ? "Admin" : "User" }}
|
||||
</template>
|
||||
<template v-slot:no-data>
|
||||
<v-btn color="primary" @click="initialize">
|
||||
Reset
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirmation from "@/components/UI/Confirmation";
|
||||
import api from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
export default {
|
||||
components: { Confirmation },
|
||||
mixins: [validators],
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
activeId: null,
|
||||
activeName: null,
|
||||
headers: [
|
||||
{
|
||||
text: "User ID",
|
||||
align: "start",
|
||||
sortable: false,
|
||||
value: "id",
|
||||
},
|
||||
{ text: "Full Name", value: "fullName" },
|
||||
{ text: "Email", value: "email" },
|
||||
{ text: "Family", value: "family" },
|
||||
{ text: "Admin", value: "admin" },
|
||||
{ text: "", value: "actions", sortable: false, align: "center" },
|
||||
],
|
||||
users: [],
|
||||
editedIndex: -1,
|
||||
editedItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
},
|
||||
defaultItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
},
|
||||
}),
|
||||
|
||||
computed: {
|
||||
formTitle() {
|
||||
return this.editedIndex === -1 ? "New User" : "Edit User";
|
||||
},
|
||||
showPassword() {
|
||||
return this.editedIndex === -1 ? true : false;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
dialog(val) {
|
||||
val || this.close();
|
||||
},
|
||||
dialogDelete(val) {
|
||||
val || this.closeDelete();
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initialize() {
|
||||
this.users = await api.users.allUsers();
|
||||
},
|
||||
|
||||
async deleteUser() {
|
||||
await api.users.delete(this.activeId);
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
editItem(item) {
|
||||
this.editedIndex = this.users.indexOf(item);
|
||||
this.editedItem = Object.assign({}, item);
|
||||
this.dialog = true;
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
deleteItemConfirm() {
|
||||
this.users.splice(this.editedIndex, 1);
|
||||
this.closeDelete();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.dialog = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
closeDelete() {
|
||||
this.dialogDelete = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (this.editedIndex > -1) {
|
||||
api.users.update(this.editedItem);
|
||||
this.close();
|
||||
} else if (this.$refs.newUser.validate()) {
|
||||
api.users.create(this.editedItem);
|
||||
this.close();
|
||||
}
|
||||
await this.initialize();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
218
frontend/src/components/Admin/ManageUsers/TheSignUpTable.vue
Normal file
218
frontend/src/components/Admin/ManageUsers/TheSignUpTable.vue
Normal file
|
@ -0,0 +1,218 @@
|
|||
<template>
|
||||
<v-card outlined>
|
||||
<Confirmation
|
||||
ref="deleteUserDialog"
|
||||
title="Confirm User Deletion"
|
||||
:message="`Are you sure you want to delete the link <b>${activeName}<b/>`"
|
||||
icon="mdi-alert"
|
||||
@confirm="deleteUser"
|
||||
:width="450"
|
||||
@close="closeDelete"
|
||||
/>
|
||||
<v-toolbar flat>
|
||||
<v-icon large color="accent" class="mr-1">
|
||||
mdi-link-variant
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headine">
|
||||
Sign Up Links
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer> </v-spacer>
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn small color="success" dark v-bind="attrs" v-on="on">
|
||||
Create Link
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-app-bar dark dense color="primary">
|
||||
<v-icon left>
|
||||
mdi-account
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
Create Link
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
|
||||
<v-card-text>
|
||||
<v-form ref="newUser">
|
||||
<v-text-field
|
||||
v-model="editedItem.name"
|
||||
label="Link Name"
|
||||
:rules="[existsRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="close">
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="save">
|
||||
Save
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-toolbar>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-data-table :headers="headers" :items="links" sort-by="calories">
|
||||
<template v-slot:item.token="{ item }">
|
||||
{{ `${baseURL}/user/sign-up/${item.token}` }}
|
||||
<v-btn
|
||||
icon
|
||||
class="mr-1"
|
||||
small
|
||||
color="accent"
|
||||
@click="updateClipboard(`${baseURL}/user/sign-up/${item.token}`)"
|
||||
>
|
||||
<v-icon>
|
||||
mdi-content-copy
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn class="mr-1" small color="error" @click="deleteItem(item)">
|
||||
<v-icon small left>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
Delete
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirmation from "@/components/UI/Confirmation";
|
||||
import api from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
export default {
|
||||
components: { Confirmation },
|
||||
mixins: [validators],
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
activeId: null,
|
||||
activeName: null,
|
||||
headers: [
|
||||
{
|
||||
text: "Link ID",
|
||||
align: "start",
|
||||
sortable: false,
|
||||
value: "id",
|
||||
},
|
||||
{ text: "Name", value: "name" },
|
||||
{ text: "Token", value: "token" },
|
||||
{ text: "", value: "actions", sortable: false, align: "center" },
|
||||
],
|
||||
links: [],
|
||||
editedIndex: -1,
|
||||
editedItem: {
|
||||
name: "",
|
||||
token: "",
|
||||
id: 0,
|
||||
},
|
||||
defaultItem: {
|
||||
name: "",
|
||||
token: "",
|
||||
id: 0,
|
||||
},
|
||||
}),
|
||||
|
||||
computed: {
|
||||
baseURL() {
|
||||
return window.location.origin;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
dialog(val) {
|
||||
val || this.close();
|
||||
},
|
||||
dialogDelete(val) {
|
||||
val || this.closeDelete();
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateClipboard(newClip) {
|
||||
navigator.clipboard.writeText(newClip).then(
|
||||
function() {
|
||||
console.log("Copied", newClip);
|
||||
},
|
||||
function() {
|
||||
console.log("Copy Failed", newClip);
|
||||
}
|
||||
);
|
||||
},
|
||||
async initialize() {
|
||||
this.links = await api.signUps.getAll();
|
||||
},
|
||||
|
||||
async deleteUser() {
|
||||
await api.signUps.deleteToken(this.activeId);
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
editItem(item) {
|
||||
this.editedIndex = this.links.indexOf(item);
|
||||
this.editedItem = Object.assign({}, item);
|
||||
this.dialog = true;
|
||||
},
|
||||
|
||||
deleteItem(item) {
|
||||
this.activeId = item.token;
|
||||
this.activeName = item.name;
|
||||
this.editedIndex = this.links.indexOf(item);
|
||||
this.editedItem = Object.assign({}, item);
|
||||
this.$refs.deleteUserDialog.open();
|
||||
},
|
||||
|
||||
deleteItemConfirm() {
|
||||
this.links.splice(this.editedIndex, 1);
|
||||
this.closeDelete();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.dialog = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
closeDelete() {
|
||||
this.dialogDelete = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (this.editedIndex > -1) {
|
||||
api.links.update(this.editedItem);
|
||||
this.close();
|
||||
} else if (this.$refs.newUser.validate()) {
|
||||
api.signUps.createToken({ name: this.editedItem.name });
|
||||
this.close();
|
||||
}
|
||||
await this.initialize();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
252
frontend/src/components/Admin/ManageUsers/TheUserTable.vue
Normal file
252
frontend/src/components/Admin/ManageUsers/TheUserTable.vue
Normal file
|
@ -0,0 +1,252 @@
|
|||
<template>
|
||||
<v-card outlined>
|
||||
<Confirmation
|
||||
ref="deleteUserDialog"
|
||||
title="Confirm User Deletion"
|
||||
:message="
|
||||
`Are you sure you want to delete the user <b>${activeName} ID: ${activeId}<b/>`
|
||||
"
|
||||
icon="mdi-alert"
|
||||
@confirm="deleteUser"
|
||||
:width="450"
|
||||
@close="closeDelete"
|
||||
/>
|
||||
<v-toolbar flat>
|
||||
<v-icon large color="accent" class="mr-1">
|
||||
mdi-account
|
||||
</v-icon>
|
||||
<v-toolbar-title class="headine">
|
||||
Users
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer> </v-spacer>
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn small color="success" dark v-bind="attrs" v-on="on">
|
||||
Create User
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-app-bar dark dense color="primary">
|
||||
<v-icon left>
|
||||
mdi-account
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
{{ formTitle }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-title class="headline">
|
||||
User ID: {{ editedItem.id }}
|
||||
</v-toolbar-title>
|
||||
</v-app-bar>
|
||||
|
||||
<v-card-text>
|
||||
<v-form ref="newUser">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.fullName"
|
||||
label="Full Name"
|
||||
:rules="[existsRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.email"
|
||||
label="Email"
|
||||
:rules="[existsRule, emailRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.family"
|
||||
label="Family Group"
|
||||
></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"
|
||||
:rules="[existsRule, minRule]"
|
||||
></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-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="close">
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="save">
|
||||
Save
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-toolbar>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-data-table :headers="headers" :items="users" sort-by="calories">
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn class="mr-1" small color="error" @click="deleteItem(item)">
|
||||
<v-icon small left>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
Delete
|
||||
</v-btn>
|
||||
<v-btn small color="success" @click="editItem(item)">
|
||||
<v-icon small left class="mr-2">
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
Edit
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:item.admin="{ item }">
|
||||
{{ item.admin ? "Admin" : "User" }}
|
||||
</template>
|
||||
<template v-slot:no-data>
|
||||
<v-btn color="primary" @click="initialize">
|
||||
Reset
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirmation from "@/components/UI/Confirmation";
|
||||
import api from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
export default {
|
||||
components: { Confirmation },
|
||||
mixins: [validators],
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
activeId: null,
|
||||
activeName: null,
|
||||
headers: [
|
||||
{
|
||||
text: "User ID",
|
||||
align: "start",
|
||||
sortable: false,
|
||||
value: "id",
|
||||
},
|
||||
{ text: "Full Name", value: "fullName" },
|
||||
{ text: "Email", value: "email" },
|
||||
{ text: "Family", value: "family" },
|
||||
{ text: "Admin", value: "admin" },
|
||||
{ text: "", value: "actions", sortable: false, align: "center" },
|
||||
],
|
||||
users: [],
|
||||
editedIndex: -1,
|
||||
editedItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
},
|
||||
defaultItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
},
|
||||
}),
|
||||
|
||||
computed: {
|
||||
formTitle() {
|
||||
return this.editedIndex === -1 ? "New User" : "Edit User";
|
||||
},
|
||||
showPassword() {
|
||||
return this.editedIndex === -1 ? true : false;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
dialog(val) {
|
||||
val || this.close();
|
||||
},
|
||||
dialogDelete(val) {
|
||||
val || this.closeDelete();
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initialize() {
|
||||
this.users = await api.users.allUsers();
|
||||
},
|
||||
|
||||
async deleteUser() {
|
||||
await api.users.delete(this.activeId);
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
editItem(item) {
|
||||
this.editedIndex = this.users.indexOf(item);
|
||||
this.editedItem = Object.assign({}, item);
|
||||
this.dialog = true;
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
deleteItemConfirm() {
|
||||
this.users.splice(this.editedIndex, 1);
|
||||
this.closeDelete();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.dialog = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
closeDelete() {
|
||||
this.dialogDelete = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (this.editedIndex > -1) {
|
||||
api.users.update(this.editedItem);
|
||||
this.close();
|
||||
} else if (this.$refs.newUser.validate()) {
|
||||
api.users.create(this.editedItem);
|
||||
this.close();
|
||||
}
|
||||
await this.initialize();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -1,249 +1,56 @@
|
|||
<template>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="users"
|
||||
sort-by="calories"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template v-slot:top>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>Mealie Users</v-toolbar-title>
|
||||
<v-divider class="mx-4" inset vertical></v-divider>
|
||||
<v-spacer></v-spacer>
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn color="primary" dark class="mb-2" v-bind="attrs" v-on="on">
|
||||
Create User
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-app-bar dark dense color="primary">
|
||||
<v-icon left>
|
||||
mdi-account
|
||||
</v-icon>
|
||||
<div>
|
||||
<v-card flat>
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
background-color="primary"
|
||||
centered
|
||||
dark
|
||||
icons-and-text
|
||||
>
|
||||
<v-tabs-slider></v-tabs-slider>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
{{ formTitle }}
|
||||
</v-toolbar-title>
|
||||
<v-tab>
|
||||
Users
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</v-tab>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<v-toolbar-title class="headline">
|
||||
User ID: {{ editedItem.id }}
|
||||
</v-toolbar-title>
|
||||
</v-app-bar>
|
||||
<v-tab>
|
||||
Sign-Up Links
|
||||
<v-icon>mdi-account-plus-outline</v-icon>
|
||||
</v-tab>
|
||||
|
||||
<v-card-text>
|
||||
<v-form ref="newUser">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.fullName"
|
||||
label="Full Name"
|
||||
:rules="[existsRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.email"
|
||||
label="Email"
|
||||
:rules="[existsRule, emailRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="12" md="6">
|
||||
<v-text-field
|
||||
v-model="editedItem.family"
|
||||
label="Family Group"
|
||||
></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"
|
||||
:rules="[existsRule, minRule]"
|
||||
></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-form>
|
||||
</v-card-text>
|
||||
<v-tab>
|
||||
Groups
|
||||
<v-icon>mdi-account-group</v-icon>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="close">
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn color="primary" @click="save">
|
||||
Save
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<Confirmation
|
||||
ref="deleteUserDialog"
|
||||
title="Confirm User Deletion"
|
||||
:message="
|
||||
`Are you sure you want to delete the user <b>${activeName} ID: ${activeId}<b/>`
|
||||
"
|
||||
icon="mdi-alert"
|
||||
@confirm="deleteUser"
|
||||
:width="450"
|
||||
@close="closeDelete"
|
||||
/>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn class="mr-1" small color="error" @click="deleteItem(item)">
|
||||
<v-icon small left>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
Delete
|
||||
</v-btn>
|
||||
<v-btn small color="success" @click="editItem(item)">
|
||||
<v-icon small left class="mr-2">
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
Edit
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:item.admin="{ item }">
|
||||
{{ item.admin ? "Admin" : "User" }}
|
||||
</template>
|
||||
<template v-slot:no-data>
|
||||
<v-btn color="primary" @click="initialize">
|
||||
Reset
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<v-tabs-items v-model="tab">
|
||||
<v-tab-item>
|
||||
<TheUserTable />
|
||||
</v-tab-item>
|
||||
<v-tab-item>
|
||||
<TheSignUpTable />
|
||||
</v-tab-item>
|
||||
<v-tab-item>
|
||||
<TheGroupTable />
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirmation from "@/components/UI/Confirmation";
|
||||
import api from "@/api";
|
||||
import { validators } from "@/mixins/validators";
|
||||
import TheUserTable from "@/components/Admin/ManageUsers/TheUserTable";
|
||||
import TheGroupTable from "@/components/Admin/ManageUsers/TheGroupTable";
|
||||
import TheSignUpTable from "@/components/Admin/ManageUsers/TheSignUpTable";
|
||||
export default {
|
||||
components: { Confirmation },
|
||||
mixins: [validators],
|
||||
data: () => ({
|
||||
dialog: false,
|
||||
activeId: null,
|
||||
activeName: null,
|
||||
headers: [
|
||||
{
|
||||
text: "User ID",
|
||||
align: "start",
|
||||
sortable: false,
|
||||
value: "id",
|
||||
},
|
||||
{ text: "Full Name", value: "fullName" },
|
||||
{ text: "Email", value: "email" },
|
||||
{ text: "Family", value: "family" },
|
||||
{ text: "Admin", value: "admin" },
|
||||
{ text: "", value: "actions", sortable: false, align: "center" },
|
||||
],
|
||||
users: [],
|
||||
editedIndex: -1,
|
||||
editedItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
},
|
||||
defaultItem: {
|
||||
id: 0,
|
||||
fullName: "",
|
||||
password: "",
|
||||
email: "",
|
||||
family: "",
|
||||
admin: false,
|
||||
},
|
||||
}),
|
||||
|
||||
computed: {
|
||||
formTitle() {
|
||||
return this.editedIndex === -1 ? "New User" : "Edit User";
|
||||
},
|
||||
showPassword() {
|
||||
return this.editedIndex === -1 ? true : false;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
dialog(val) {
|
||||
val || this.close();
|
||||
},
|
||||
dialogDelete(val) {
|
||||
val || this.closeDelete();
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initialize() {
|
||||
this.users = await api.users.allUsers();
|
||||
},
|
||||
|
||||
async deleteUser() {
|
||||
await api.users.delete(this.activeId);
|
||||
this.initialize();
|
||||
},
|
||||
|
||||
editItem(item) {
|
||||
this.editedIndex = this.users.indexOf(item);
|
||||
this.editedItem = Object.assign({}, item);
|
||||
this.dialog = true;
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
deleteItemConfirm() {
|
||||
this.users.splice(this.editedIndex, 1);
|
||||
this.closeDelete();
|
||||
},
|
||||
|
||||
close() {
|
||||
this.dialog = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
closeDelete() {
|
||||
this.dialogDelete = false;
|
||||
this.$nextTick(() => {
|
||||
this.editedItem = Object.assign({}, this.defaultItem);
|
||||
this.editedIndex = -1;
|
||||
});
|
||||
},
|
||||
|
||||
async save() {
|
||||
if (this.editedIndex > -1) {
|
||||
api.users.update(this.editedItem);
|
||||
this.close();
|
||||
} else if (this.$refs.newUser.validate()) {
|
||||
api.users.create(this.editedItem);
|
||||
this.close();
|
||||
}
|
||||
await this.initialize();
|
||||
},
|
||||
components: { TheUserTable, TheGroupTable, TheSignUpTable },
|
||||
data() {
|
||||
return {
|
||||
tab: 0,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -4,6 +4,7 @@ from db.db_base import BaseDocument
|
|||
from db.models.mealplan import MealPlanModel
|
||||
from db.models.recipe import Category, RecipeModel, Tag
|
||||
from db.models.settings import SiteSettingsModel
|
||||
from db.models.sign_up import SignUp
|
||||
from db.models.theme import SiteThemeModel
|
||||
from db.models.users import User
|
||||
|
||||
|
@ -70,6 +71,11 @@ class _Users(BaseDocument):
|
|||
return return_data
|
||||
|
||||
|
||||
class _SignUps(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "token"
|
||||
self.sql_model = SignUp
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self) -> None:
|
||||
|
@ -80,6 +86,7 @@ class Database:
|
|||
self.categories = _Categories()
|
||||
self.tags = _Tags()
|
||||
self.users = _Users()
|
||||
self.sign_ups = _SignUps()
|
||||
|
||||
|
||||
db = Database()
|
||||
|
|
|
@ -3,3 +3,4 @@ from db.models.recipe import *
|
|||
from db.models.settings import *
|
||||
from db.models.theme import *
|
||||
from db.models.users import *
|
||||
from db.models.sign_up import *
|
||||
|
|
25
mealie/db/models/sign_up.py
Normal file
25
mealie/db/models/sign_up.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
from sqlalchemy import Column, Integer, String
|
||||
|
||||
|
||||
class SignUp(SqlAlchemyBase, BaseMixins):
|
||||
__tablename__ = "sign_ups"
|
||||
id = Column(Integer, primary_key=True)
|
||||
token = Column(String, nullable=False, index=True)
|
||||
name = Column(String, index=True)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session,
|
||||
token,
|
||||
name,
|
||||
) -> None:
|
||||
self.token = token
|
||||
self.name = name
|
||||
|
||||
def dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"token": self.token,
|
||||
}
|
|
@ -20,4 +20,5 @@ def query_user(user_email: str, session: Session = None) -> UserInDB:
|
|||
session = session if session else create_session()
|
||||
user = db.users.get(session, user_email, "email")
|
||||
session.close()
|
||||
return UserInDB(**user)
|
||||
return UserInDB(**user)
|
||||
|
||||
|
|
81
mealie/routes/users/sign_up.py
Normal file
81
mealie/routes/users/sign_up.py
Normal file
|
@ -0,0 +1,81 @@
|
|||
import uuid
|
||||
|
||||
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 schema.sign_up import SignUpIn, SignUpOut, SignUpToken
|
||||
from schema.snackbar import SnackResponse
|
||||
from schema.user import UserIn, UserInDB
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
router = APIRouter(prefix="/api/users/sign-ups", tags=["User Signup"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[SignUpOut])
|
||||
async def get_all_open_sign_ups(
|
||||
current_user=Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Returns a list of open sign up links """
|
||||
|
||||
all_sign_ups = db.sign_ups.get_all(session)
|
||||
|
||||
return all_sign_ups
|
||||
|
||||
|
||||
@router.post("", response_model=SignUpToken)
|
||||
async def create_user_sign_up_key(
|
||||
key_data: SignUpIn,
|
||||
current_user: UserInDB = Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Generates a Random Token that a new user can sign up with """
|
||||
|
||||
if current_user.admin:
|
||||
sign_up = {"token": str(uuid.uuid1().hex), "name": key_data.name}
|
||||
db_entry = db.sign_ups.create(session, sign_up)
|
||||
|
||||
return db_entry
|
||||
|
||||
else:
|
||||
return {"details": "not authorized"}
|
||||
|
||||
|
||||
@router.post("/{token}")
|
||||
async def create_user_with_token(
|
||||
token: str,
|
||||
new_user: UserIn,
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Creates a user with a valid sign up token """
|
||||
|
||||
# Validate Token
|
||||
db_entry = db.sign_ups.get(session, token, limit=1)
|
||||
if not db_entry:
|
||||
return {"details": "invalid token"}
|
||||
|
||||
# Create User
|
||||
new_user.password = get_password_hash(new_user.password)
|
||||
data = db.users.create(session, new_user.dict())
|
||||
|
||||
# DeleteToken
|
||||
db.sign_ups.delete(session, token)
|
||||
|
||||
# Respond
|
||||
return SnackResponse.success(f"User Created: {new_user.full_name}", data)
|
||||
|
||||
|
||||
@router.delete("/{token}")
|
||||
async def delete_token(
|
||||
token: str,
|
||||
current_user: UserInDB = Depends(manager),
|
||||
session: Session = Depends(generate_session),
|
||||
):
|
||||
""" Removed a token from the database """
|
||||
if current_user.admin:
|
||||
db.sign_ups.delete(session, token)
|
||||
return SnackResponse.error("Sign Up Token Deleted")
|
||||
else:
|
||||
return {"details", "not authorized"}
|
|
@ -1,7 +1,8 @@
|
|||
from fastapi import APIRouter
|
||||
from routes.users import auth, crud
|
||||
from routes.users import auth, crud, sign_up
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(sign_up.router)
|
||||
router.include_router(auth.router)
|
||||
router.include_router(crud.router)
|
||||
|
|
13
mealie/schema/sign_up.py
Normal file
13
mealie/schema/sign_up.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from fastapi_camelcase import CamelModel
|
||||
|
||||
|
||||
class SignUpIn(CamelModel):
|
||||
name: str
|
||||
|
||||
|
||||
class SignUpToken(SignUpIn):
|
||||
token: str
|
||||
|
||||
|
||||
class SignUpOut(SignUpToken):
|
||||
id: int
|
Loading…
Add table
Add a link
Reference in a new issue