mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 14:33:33 -07:00
Feature/authentication (#195)
* basic crud NOT SECURE * refactor/database init on startup * added scratch.py * tests/user CRUD routes * password hashing * change app_config location * bump python version * formatting * login ui starter * change import from url design * move components * remove old snackbar * refactor/Componenet folder structure rework * refactor/remove old code * refactor/rename componenets/js files * remove console.logs * refactor/ models to schema and sql to models * new header styling for imports * token request * fix url scrapper * refactor/rename schema files * split routes file * redesigned admin page * enable relative imports for vue components * refactor/switch to pages view * add CamelCase package * majors settings rework * user management second pass * super user CRUD * refactor/consistent models names * refactor/consistent model names * password reset * store refactor * dependency update * abstract button props * profile page refactor * basic password validation * login form refactor/split v-container * remo unused code * hide editor buttons when not logged in * mkdocs dev dependency * v0.4.0 docs update * profile image upload * additional token routes * Smaller recipe cards for smaller viewports * fix admin sidebar * add users * change to outlined * theme card starter * code cleanup * signups * signup pages * fix #194 * fix #193 * clarify mealie_port * fix #184 * fixes #178 * fix blank card error on meal-plan creator * admin signup Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
parent
9af664c259
commit
95b1ab0dec
30 changed files with 1323 additions and 354 deletions
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
auto_https off
|
||||
admin off
|
||||
}
|
||||
|
||||
:80 {
|
||||
|
|
|
@ -41,12 +41,12 @@ services:
|
|||
|
||||
## Env Variables
|
||||
|
||||
| Variables | default | description |
|
||||
| ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| db_type | sqlite | The database type to be used. Current Options 'sqlite' |
|
||||
| mealie_port | 9000 | The port exposed by mealie. **do not change this if you're running in docker** If you'd like to use another port, map 9000 to another port of the host. |
|
||||
| api_docs | True | Turns on/off access to the API documentation locally. |
|
||||
| TZ | UTC | You should set your time zone accordingly so the date/time features work correctly |
|
||||
| Variables | default | description |
|
||||
| ----------- | ------- | ----------------------------------------------------------------------------------- |
|
||||
| db_type | sqlite | The database type to be used. Current Options 'sqlite' |
|
||||
| mealie_port | 9000 | The port exposed by backend API. **do not change this if you're running in docker** |
|
||||
| api_docs | True | Turns on/off access to the API documentation locally. |
|
||||
| TZ | UTC | You should set your time zone accordingly so the date/time features work correctly |
|
||||
|
||||
|
||||
## Deployed as a Python Application
|
||||
|
|
|
@ -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 class="mt-n1">
|
||||
<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>
|
239
frontend/src/components/Admin/ManageUsers/TheSignUpTable.vue
Normal file
239
frontend/src/components/Admin/ManageUsers/TheSignUpTable.vue
Normal file
|
@ -0,0 +1,239 @@
|
|||
<template>
|
||||
<v-card outlined class="mt-n1">
|
||||
<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-row class="justify-center mt-3">
|
||||
<v-text-field
|
||||
class="mr-2"
|
||||
v-model="editedItem.name"
|
||||
label="Link Name"
|
||||
:rules="[existsRule]"
|
||||
validate-on-blur
|
||||
></v-text-field>
|
||||
<v-checkbox
|
||||
v-model="editedItem.admin"
|
||||
label="Admin"
|
||||
></v-checkbox>
|
||||
</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="links" sort-by="calories">
|
||||
<template v-slot:item.token="{ item }">
|
||||
{{ `${baseURL}/sign-up/${item.token}` }}
|
||||
<v-btn
|
||||
icon
|
||||
class="mr-1"
|
||||
small
|
||||
color="accent"
|
||||
@click="updateClipboard(`${baseURL}/sign-up/${item.token}`)"
|
||||
>
|
||||
<v-icon>
|
||||
mdi-content-copy
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:item.admin="{ item }">
|
||||
<v-btn small :color="item.admin ? 'success' : 'error'" text>
|
||||
<v-icon small left>
|
||||
mdi-account-cog
|
||||
</v-icon>
|
||||
{{ item.admin ? "Yes" : "No" }}
|
||||
</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: "Admin", value: "admin", align: "center" },
|
||||
{ text: "", value: "actions", sortable: false, align: "center" },
|
||||
],
|
||||
links: [],
|
||||
editedIndex: -1,
|
||||
editedItem: {
|
||||
name: "",
|
||||
admin: false,
|
||||
token: "",
|
||||
id: 0,
|
||||
},
|
||||
defaultItem: {
|
||||
name: "",
|
||||
token: "",
|
||||
admin: false,
|
||||
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,
|
||||
admin: this.editedItem.admin,
|
||||
});
|
||||
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 class="mt-n1">
|
||||
<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,9 +1,22 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-btn text color="info" @click="dialog = true"> {{$t('general.new')}} </v-btn>
|
||||
<v-dialog v-model="dialog" width="400">
|
||||
<v-btn text color="info" @click="dialog = true">
|
||||
{{ $t("settings.add-a-new-theme") }}
|
||||
</v-btn>
|
||||
<v-dialog v-model="dialog" width="500">
|
||||
<v-card>
|
||||
<v-card-title> {{$t('settings.add-a-new-theme')}} </v-card-title>
|
||||
<v-app-bar dense dark color="primary mb-2">
|
||||
<v-icon large left class="mt-1">
|
||||
mdi-format-color-fill
|
||||
</v-icon>
|
||||
|
||||
<v-toolbar-title class="headline">
|
||||
{{ $t("settings.add-a-new-theme") }}
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-card-title> </v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
:label="$t('settings.theme.theme-name')"
|
||||
|
@ -13,9 +26,11 @@
|
|||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" text @click="dialog = false"> {{$t('general.cancel')}} </v-btn>
|
||||
<v-btn color="grey" text @click="dialog = false">
|
||||
{{ $t("general.cancel") }}
|
||||
</v-btn>
|
||||
<v-btn color="success" text @click="Select" :disabled="!themeName">
|
||||
{{$t('general.create')}}
|
||||
{{ $t("general.create") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
@ -34,7 +49,8 @@ export default {
|
|||
dialog: false,
|
||||
themeName: "",
|
||||
rules: {
|
||||
required: (val) => !!val || this.$t("settings.theme.theme-name-is-required"),
|
||||
required: val =>
|
||||
!!val || this.$t("settings.theme.theme-name-is-required"),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
84
frontend/src/components/Admin/Theme/ThemeCard.vue
Normal file
84
frontend/src/components/Admin/Theme/ThemeCard.vue
Normal file
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<div>
|
||||
<Confirmation
|
||||
:title="$t('settings.theme.delete-theme')"
|
||||
:message="$t('settings.theme.are-you-sure-you-want-to-delete-this-theme')"
|
||||
color="error"
|
||||
icon="mdi-alert-circle"
|
||||
ref="deleteThemeConfirm"
|
||||
v-on:confirm="deleteSelectedTheme()"
|
||||
/>
|
||||
<v-card flat outlined class="ma-2">
|
||||
<v-card-text class="mb-n5 mt-n2">
|
||||
<h3>{{ theme.name }} {{ current ? "(Current)" : "" }}</h3>
|
||||
</v-card-text>
|
||||
<v-card-text>
|
||||
<v-row dense>
|
||||
<v-card
|
||||
v-for="(color, index) in theme.colors"
|
||||
:key="index"
|
||||
class="mx-1"
|
||||
height="34"
|
||||
width="36"
|
||||
:color="color"
|
||||
>
|
||||
</v-card>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-btn text color="error" @click="confirmDelete"> Delete </v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<!-- <v-btn text color="accent" @click="editTheme">Edit</v-btn> -->
|
||||
<v-btn text color="success" @click="saveThemes">Apply</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Confirmation from "@/components/UI/Confirmation";
|
||||
import api from "@/api";
|
||||
|
||||
const DELETE_EVENT = "delete";
|
||||
const APPLY_EVENT = "apply";
|
||||
const EDIT_EVENT = "edit";
|
||||
export default {
|
||||
components: {
|
||||
Confirmation,
|
||||
},
|
||||
props: {
|
||||
theme: Object,
|
||||
current: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
confirmDelete() {
|
||||
if (this.theme.name === "default") {
|
||||
// Notify User Can't Delete Default
|
||||
} else if (this.theme !== {}) {
|
||||
this.$refs.deleteThemeConfirm.open();
|
||||
}
|
||||
},
|
||||
async deleteSelectedTheme() {
|
||||
//Delete Theme from DB
|
||||
await api.themes.delete(this.theme.name);
|
||||
|
||||
//Get the new list of available from DB
|
||||
this.availableThemes = await api.themes.requestAll();
|
||||
this.$emit(DELETE_EVENT);
|
||||
},
|
||||
async saveThemes() {
|
||||
this.$store.commit("setTheme", this.theme);
|
||||
this.$emit(APPLY_EVENT, this.theme);
|
||||
},
|
||||
editTheme() {
|
||||
this.$emit(EDIT_EVENT);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<v-card width="500px">
|
||||
<v-divider></v-divider>
|
||||
<v-app-bar dark color="primary" class="mt-n1 mb-4">
|
||||
<v-app-bar dark color="primary" class="mt-n1 mb-2">
|
||||
<v-icon large left v-if="!loading">
|
||||
mdi-account
|
||||
</v-icon>
|
||||
|
@ -14,10 +14,8 @@
|
|||
>
|
||||
</v-progress-circular>
|
||||
<v-toolbar-title class="headline"> Login </v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
|
||||
<v-card-text>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
|
@ -45,6 +43,8 @@
|
|||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword = !showPassword"
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
v-if="options.isLoggingIn"
|
||||
@click.prevent="login"
|
||||
|
@ -54,7 +54,7 @@
|
|||
type="submit"
|
||||
>{{ $t("login.sign-in") }}</v-btn
|
||||
>
|
||||
</v-form>
|
||||
</v-card-actions>
|
||||
<v-alert v-if="error" outlined class="mt-3 mb-0" type="error">
|
||||
Could Not Validate Credentials
|
||||
</v-alert>
|
||||
|
|
139
frontend/src/components/Login/SignUpForm.vue
Normal file
139
frontend/src/components/Login/SignUpForm.vue
Normal file
|
@ -0,0 +1,139 @@
|
|||
<template>
|
||||
<v-card width="500px">
|
||||
<v-divider></v-divider>
|
||||
<v-app-bar dark color="primary" class="mt-n1">
|
||||
<v-icon large left v-if="!loading">
|
||||
mdi-account
|
||||
</v-icon>
|
||||
<v-progress-circular
|
||||
v-else
|
||||
indeterminate
|
||||
color="white"
|
||||
large
|
||||
class="mr-2"
|
||||
>
|
||||
</v-progress-circular>
|
||||
<v-toolbar-title class="headline"> Sign Up </v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
</v-app-bar>
|
||||
<v-card-text>
|
||||
Welcome to Mealie! To become a user of this instance you are required to
|
||||
have a valid invitation link. If you haven't recieved an invitation you
|
||||
are unable to sign-up. To recieve a link, contact the sites administrator.
|
||||
<v-divider class="mt-3"></v-divider>
|
||||
<v-form>
|
||||
<v-text-field
|
||||
v-model="user.name"
|
||||
light="light"
|
||||
prepend-icon="mdi-account"
|
||||
validate-on-blur
|
||||
label="Display Name"
|
||||
type="email"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.email"
|
||||
light="light"
|
||||
prepend-icon="mdi-email"
|
||||
validate-on-blur
|
||||
:label="$t('login.email')"
|
||||
type="email"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.password"
|
||||
light="light"
|
||||
class="mb-2s"
|
||||
prepend-icon="mdi-lock"
|
||||
:label="$t('login.password')"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword = !showPassword"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="user.passwordConfirm"
|
||||
light="light"
|
||||
class="mb-2s"
|
||||
prepend-icon="mdi-lock"
|
||||
:label="$t('login.password')"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword = !showPassword"
|
||||
></v-text-field>
|
||||
</v-form>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
v-if="options.isLoggingIn"
|
||||
@click.prevent="signUp"
|
||||
dark
|
||||
color="primary"
|
||||
block="block"
|
||||
type="submit"
|
||||
>
|
||||
Sign Up
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
<v-alert dense v-if="error" outlined class="mt-3 mb-0" type="error">
|
||||
Error Signing Up
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from "@/api";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
error: false,
|
||||
showPassword: false,
|
||||
user: {
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
passwordConfirm: "",
|
||||
},
|
||||
options: {
|
||||
isLoggingIn: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.clear();
|
||||
},
|
||||
computed: {
|
||||
token() {
|
||||
return this.$route.params.token;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
this.user = {
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
passwordConfirm: "",
|
||||
};
|
||||
},
|
||||
async signUp() {
|
||||
this.loading = true;
|
||||
this.error = false;
|
||||
|
||||
const userData = {
|
||||
fullName: this.user.name,
|
||||
email: this.user.email,
|
||||
family: "public",
|
||||
password: this.user.password,
|
||||
admin: false,
|
||||
};
|
||||
|
||||
await api.signUps.createUser(this.token, userData);
|
||||
this.$emit("user-created");
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -65,7 +65,7 @@
|
|||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<v-card-text v-if="startDate">
|
||||
<MealPlanCard v-model="meals" />
|
||||
</v-card-text>
|
||||
<v-row align="center" justify="end">
|
||||
|
@ -133,6 +133,7 @@ export default {
|
|||
|
||||
let dateDif = (endDate - startDate) / (1000 * 3600 * 24) + 1;
|
||||
|
||||
|
||||
if (dateDif < 1) {
|
||||
return null;
|
||||
}
|
||||
|
@ -182,9 +183,9 @@ export default {
|
|||
};
|
||||
await api.mealPlans.create(mealBody);
|
||||
this.$emit("created");
|
||||
this.meals = [];
|
||||
this.startDate = null;
|
||||
this.endDate = null;
|
||||
this.meals = [];
|
||||
},
|
||||
|
||||
getImage(image) {
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<template>
|
||||
<v-card :to="`/recipe/${slug}`" max-height="125">
|
||||
<v-card hover :to="`/recipe/${slug}`" max-height="125">
|
||||
<v-list-item>
|
||||
<v-list-item-avatar rounded size="125" class="mt-0 ml-n4">
|
||||
<v-img :src="getImage(image)"> </v-img>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-content class="align-self-start">
|
||||
<v-list-item-title>
|
||||
{{ name }}
|
||||
</v-list-item-title>
|
||||
<v-rating length="5" size="16" dense :value="rating"></v-rating>
|
||||
<div class="text">
|
||||
<v-list-item-action-text>
|
||||
{{ description | truncate(50) }}
|
||||
{{ description | truncate(115) }}
|
||||
</v-list-item-action-text>
|
||||
</div>
|
||||
</v-list-item-content>
|
||||
|
@ -59,4 +59,8 @@ export default {
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.text-top {
|
||||
align-self: start !important;
|
||||
}
|
||||
</style>
|
|
@ -4,6 +4,7 @@
|
|||
class="custom-transparent d-flex justify-start align-center text-center "
|
||||
tile
|
||||
:width="`${timeCardWidth}`"
|
||||
height="55"
|
||||
v-if="totalTime || prepTime || performTime"
|
||||
>
|
||||
<v-card flat color="rgb(255, 0, 0, 0.0)">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -56,42 +56,6 @@
|
|||
}}
|
||||
</p>
|
||||
|
||||
<v-form ref="form" lazy-validation>
|
||||
<v-row dense align="center">
|
||||
<v-col cols="12" md="4" sm="3">
|
||||
<v-select
|
||||
:label="$t('settings.theme.saved-color-theme')"
|
||||
:items="availableThemes"
|
||||
item-text="name"
|
||||
return-object
|
||||
v-model="selectedTheme"
|
||||
@change="themeSelected"
|
||||
:rules="[v => !!v || $t('settings.theme.theme-is-required')]"
|
||||
required
|
||||
>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn-toggle group class="mt-n5">
|
||||
<NewThemeDialog @new-theme="appendTheme" class="mt-1" />
|
||||
<v-btn text color="error" @click="deleteSelectedThemeValidation">
|
||||
{{ $t("general.delete") }}
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<Confirmation
|
||||
:title="$t('settings.theme.delete-theme')"
|
||||
:message="
|
||||
$t('settings.theme.are-you-sure-you-want-to-delete-this-theme')
|
||||
"
|
||||
color="error"
|
||||
icon="mdi-alert-circle"
|
||||
ref="deleteThemeConfirm"
|
||||
v-on:confirm="deleteSelectedTheme()"
|
||||
/>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
</v-row>
|
||||
</v-form>
|
||||
<v-row dense align-content="center" v-if="selectedTheme.colors">
|
||||
<v-col>
|
||||
<ColorPickerDialog
|
||||
|
@ -138,7 +102,28 @@
|
|||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
xl="3"
|
||||
v-for="theme in availableThemes"
|
||||
:key="theme.name"
|
||||
>
|
||||
<ThemeCard
|
||||
:theme="theme"
|
||||
:current="selectedTheme.name == theme.name ? true : false"
|
||||
@delete="getAllThemes"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<NewThemeDialog @new-theme="appendTheme" class="mt-1" />
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="success" @click="saveThemes" class="mr-2">
|
||||
<v-icon left> mdi-content-save </v-icon>
|
||||
|
@ -152,59 +137,35 @@
|
|||
import api from "@/api";
|
||||
import ColorPickerDialog from "@/components/Admin/Theme/ColorPickerDialog";
|
||||
import NewThemeDialog from "@/components/Admin/Theme/NewThemeDialog";
|
||||
import Confirmation from "@/components/UI/Confirmation";
|
||||
import ThemeCard from "@/components/Admin/Theme/ThemeCard";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColorPickerDialog,
|
||||
Confirmation,
|
||||
NewThemeDialog,
|
||||
ThemeCard,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTheme: {},
|
||||
selectedDarkMode: "system",
|
||||
availableThemes: [],
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.availableThemes = await api.themes.requestAll();
|
||||
this.selectedTheme = this.$store.getters.getActiveTheme;
|
||||
await this.getAllThemes();
|
||||
|
||||
this.selectedDarkMode = this.$store.getters.getDarkMode;
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Open the delete confirmation.
|
||||
*/
|
||||
deleteSelectedThemeValidation() {
|
||||
if (this.$refs.form.validate()) {
|
||||
if (this.selectedTheme.name === "default") {
|
||||
// Notify User Can't Delete Default
|
||||
} else if (this.selectedTheme !== {}) {
|
||||
this.$refs.deleteThemeConfirm.open();
|
||||
}
|
||||
}
|
||||
computed: {
|
||||
selectedTheme() {
|
||||
return this.$store.getters.getActiveTheme;
|
||||
},
|
||||
/**
|
||||
* Delete the selected Theme
|
||||
*/
|
||||
async deleteSelectedTheme() {
|
||||
//Delete Theme from DB
|
||||
await api.themes.delete(this.selectedTheme.name);
|
||||
},
|
||||
|
||||
//Get the new list of available from DB
|
||||
methods: {
|
||||
async getAllThemes() {
|
||||
this.availableThemes = await api.themes.requestAll();
|
||||
|
||||
//Change to default if deleting current theme.
|
||||
if (
|
||||
!this.availableThemes.some(
|
||||
theme => theme.name === this.selectedTheme.name
|
||||
)
|
||||
) {
|
||||
await this.$store.dispatch("resetTheme");
|
||||
this.selectedTheme = this.$store.getters.getActiveTheme;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Create the new Theme and select it.
|
||||
|
@ -212,14 +173,8 @@ export default {
|
|||
async appendTheme(NewThemeDialog) {
|
||||
await api.themes.create(NewThemeDialog);
|
||||
this.availableThemes.push(NewThemeDialog);
|
||||
this.selectedTheme = NewThemeDialog;
|
||||
this.$store.commit("setTheme", NewThemeDialog);
|
||||
},
|
||||
|
||||
themeSelected() {
|
||||
//TODO Revamp Theme selection.
|
||||
//console.log("this.activeTheme", this.selectedTheme);
|
||||
},
|
||||
|
||||
setStoresDarkMode() {
|
||||
this.$store.commit("setDarkMode", this.selectedDarkMode);
|
||||
},
|
||||
|
@ -227,13 +182,10 @@ export default {
|
|||
* This will save the current colors and make the selected theme live.
|
||||
*/
|
||||
async saveThemes() {
|
||||
if (this.$refs.form.validate()) {
|
||||
this.$store.commit("setTheme", this.selectedTheme);
|
||||
await api.themes.update(
|
||||
this.selectedTheme.name,
|
||||
this.selectedTheme.colors
|
||||
);
|
||||
}
|
||||
await api.themes.update(
|
||||
this.selectedTheme.name,
|
||||
this.selectedTheme.colors
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<v-list-item
|
||||
v-for="(meal, index) in mealplan.meals"
|
||||
:key="generateKey(meal.slug, index)"
|
||||
@click="$router.push(`/recipe/${meal.slug}`)"
|
||||
:to="meal.slug ? `/recipe/${meal.slug}` : null"
|
||||
>
|
||||
<v-list-item-avatar
|
||||
color="primary"
|
||||
|
@ -131,8 +131,8 @@ export default {
|
|||
this.editMealPlan = null;
|
||||
this.requestMeals();
|
||||
},
|
||||
deletePlan(id) {
|
||||
api.mealPlans.delete(id);
|
||||
async deletePlan(id) {
|
||||
await api.mealPlans.delete(id);
|
||||
this.requestMeals();
|
||||
},
|
||||
openShoppingList(id) {
|
||||
|
|
41
frontend/src/pages/SignUpPage.vue
Normal file
41
frontend/src/pages/SignUpPage.vue
Normal file
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<v-container fill-height class="text-center">
|
||||
<v-row>
|
||||
<v-flex class="d-flex justify-center" width="500px">
|
||||
<SignUpForm @logged-in="redirectMe" class="ma-1" />
|
||||
</v-flex>
|
||||
</v-row>
|
||||
<v-row></v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SignUpForm from "../components/Login/SignUpForm";
|
||||
export default {
|
||||
components: {
|
||||
SignUpForm,
|
||||
},
|
||||
computed: {
|
||||
viewScale() {
|
||||
switch (this.$vuetify.breakpoint.name) {
|
||||
case "xs":
|
||||
return true;
|
||||
case "sm":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
redirectMe() {
|
||||
if (this.$route.query.redirect) {
|
||||
this.$router.push(this.$route.query.redirect);
|
||||
} else this.$router.push({ path: "/" });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -8,6 +8,7 @@ import CategoryPage from "../pages/CategoryPage";
|
|||
import MeaplPlanPage from "../pages/MealPlanPage";
|
||||
import Debug from "../pages/Debug";
|
||||
import LoginPage from "../pages/LoginPage";
|
||||
import SignUpPage from "../pages/SignUpPage";
|
||||
import MealPlanThisWeekPage from "../pages/MealPlanThisWeekPage";
|
||||
import api from "@/api";
|
||||
import Admin from "./admin";
|
||||
|
@ -25,6 +26,8 @@ export const routes = [
|
|||
},
|
||||
{ path: "/mealie", component: HomePage },
|
||||
{ path: "/login", component: LoginPage },
|
||||
{ path: "/sign-up", redirect: "/" },
|
||||
{ path: "/sign-up/:token", component: SignUpPage },
|
||||
{ path: "/debug", component: Debug },
|
||||
{ path: "/search", component: SearchPage },
|
||||
{ path: "/recipes/all", component: AllRecipesPage },
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -71,6 +72,12 @@ class _Users(BaseDocument):
|
|||
|
||||
|
||||
|
||||
class _SignUps(BaseDocument):
|
||||
def __init__(self) -> None:
|
||||
self.primary_key = "token"
|
||||
self.sql_model = SignUp
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self) -> None:
|
||||
self.recipes = _Recipes()
|
||||
|
@ -80,6 +87,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 *
|
||||
|
|
29
mealie/db/models/sign_up.py
Normal file
29
mealie/db/models/sign_up.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
from db.models.model_base import BaseMixins, SqlAlchemyBase
|
||||
from sqlalchemy import Column, Integer, String, Boolean
|
||||
|
||||
|
||||
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)
|
||||
admin = Column(Boolean, default=False)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
session,
|
||||
token,
|
||||
name,
|
||||
admin,
|
||||
) -> None:
|
||||
self.token = token
|
||||
self.name = name
|
||||
self.admin = admin
|
||||
|
||||
def dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"token": self.token,
|
||||
"admin": self.admin
|
||||
}
|
|
@ -21,3 +21,4 @@ def query_user(user_email: str, session: Session = None) -> UserInDB:
|
|||
user = db.users.get(session, user_email, "email")
|
||||
session.close()
|
||||
return UserInDB(**user)
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@ from typing import List
|
|||
from db.database import db
|
||||
from db.db_setup import generate_session
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from schema.snackbar import SnackResponse
|
||||
from services.meal_services import MealPlan
|
||||
from sqlalchemy.orm.session import Session
|
||||
from schema.snackbar import SnackResponse
|
||||
|
||||
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
|
||||
|
||||
|
@ -27,6 +27,7 @@ def get_shopping_list(id: str, session: Session = Depends(generate_session)):
|
|||
ingredients = [
|
||||
{"name": x.get("name"), "recipeIngredient": x.get("recipeIngredient")}
|
||||
for x in recipes
|
||||
if x
|
||||
]
|
||||
|
||||
return ingredients
|
||||
|
|
86
mealie/routes/users/sign_up.py
Normal file
86
mealie/routes/users/sign_up.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
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,
|
||||
"admin": key_data.admin,
|
||||
}
|
||||
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.admin = db_entry.get("admin")
|
||||
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)
|
||||
|
|
14
mealie/schema/sign_up.py
Normal file
14
mealie/schema/sign_up.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from fastapi_camelcase import CamelModel
|
||||
|
||||
|
||||
class SignUpIn(CamelModel):
|
||||
name: str
|
||||
admin: bool
|
||||
|
||||
|
||||
class SignUpToken(SignUpIn):
|
||||
token: str
|
||||
|
||||
|
||||
class SignUpOut(SignUpToken):
|
||||
id: int
|
|
@ -37,8 +37,8 @@ class Cleaner:
|
|||
recipe_data["recipeInstructions"] = Cleaner.instructions(
|
||||
recipe_data["recipeInstructions"]
|
||||
)
|
||||
recipe_data["image"] = Cleaner.image(recipe_data["image"])
|
||||
recipe_data["slug"] = slugify(recipe_data["name"])
|
||||
recipe_data["image"] = Cleaner.image(recipe_data.get("image"))
|
||||
recipe_data["slug"] = slugify(recipe_data.get("name"))
|
||||
recipe_data["orgURL"] = url
|
||||
|
||||
return recipe_data
|
||||
|
@ -50,7 +50,9 @@ class Cleaner:
|
|||
return cleantext
|
||||
|
||||
@staticmethod
|
||||
def image(image) -> str:
|
||||
def image(image=None) -> str:
|
||||
if not image:
|
||||
return "no image"
|
||||
if type(image) == list:
|
||||
return image[0]
|
||||
elif type(image) == dict:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue