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
|
auto_https off
|
||||||
|
admin off
|
||||||
}
|
}
|
||||||
|
|
||||||
:80 {
|
:80 {
|
||||||
|
|
|
@ -41,12 +41,12 @@ services:
|
||||||
|
|
||||||
## Env Variables
|
## Env Variables
|
||||||
|
|
||||||
| Variables | default | description |
|
| Variables | default | description |
|
||||||
| ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ----------- | ------- | ----------------------------------------------------------------------------------- |
|
||||||
| db_type | sqlite | The database type to be used. Current Options 'sqlite' |
|
| 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. |
|
| 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. |
|
| 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 |
|
| TZ | UTC | You should set your time zone accordingly so the date/time features work correctly |
|
||||||
|
|
||||||
|
|
||||||
## Deployed as a Python Application
|
## Deployed as a Python Application
|
||||||
|
|
|
@ -86,6 +86,8 @@ export default {
|
||||||
search: false,
|
search: false,
|
||||||
}),
|
}),
|
||||||
methods: {
|
methods: {
|
||||||
|
// For Later!
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if 'system' is set for dark mode and then sets the corrisponding value for vuetify
|
* 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 myUtils from "./upload";
|
||||||
import category from "./category";
|
import category from "./category";
|
||||||
import meta from "./meta";
|
import meta from "./meta";
|
||||||
import users from "./users"
|
import users from "./users";
|
||||||
|
import signUps from "./signUps";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
recipes: recipe,
|
recipes: recipe,
|
||||||
|
@ -20,5 +20,6 @@ export default {
|
||||||
utils: myUtils,
|
utils: myUtils,
|
||||||
categories: category,
|
categories: category,
|
||||||
meta: meta,
|
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`,
|
token: `${authPrefix}/token`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const usersURLs = {
|
const usersURLs = {
|
||||||
users: `${userPrefix}`,
|
users: `${userPrefix}`,
|
||||||
self: `${userPrefix}/self`,
|
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>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-btn text color="info" @click="dialog = true"> {{$t('general.new')}} </v-btn>
|
<v-btn text color="info" @click="dialog = true">
|
||||||
<v-dialog v-model="dialog" width="400">
|
{{ $t("settings.add-a-new-theme") }}
|
||||||
|
</v-btn>
|
||||||
|
<v-dialog v-model="dialog" width="500">
|
||||||
<v-card>
|
<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-card-text>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
:label="$t('settings.theme.theme-name')"
|
:label="$t('settings.theme.theme-name')"
|
||||||
|
@ -13,9 +26,11 @@
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<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">
|
<v-btn color="success" text @click="Select" :disabled="!themeName">
|
||||||
{{$t('general.create')}}
|
{{ $t("general.create") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
@ -34,7 +49,8 @@ export default {
|
||||||
dialog: false,
|
dialog: false,
|
||||||
themeName: "",
|
themeName: "",
|
||||||
rules: {
|
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>
|
<template>
|
||||||
<v-card width="500px">
|
<v-card width="500px">
|
||||||
<v-divider></v-divider>
|
<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">
|
<v-icon large left v-if="!loading">
|
||||||
mdi-account
|
mdi-account
|
||||||
</v-icon>
|
</v-icon>
|
||||||
|
@ -14,10 +14,8 @@
|
||||||
>
|
>
|
||||||
</v-progress-circular>
|
</v-progress-circular>
|
||||||
<v-toolbar-title class="headline"> Login </v-toolbar-title>
|
<v-toolbar-title class="headline"> Login </v-toolbar-title>
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-form>
|
<v-form>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
|
@ -45,6 +43,8 @@
|
||||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||||
@click:append="showPassword = !showPassword"
|
@click:append="showPassword = !showPassword"
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
|
</v-form>
|
||||||
|
<v-card-actions>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="options.isLoggingIn"
|
v-if="options.isLoggingIn"
|
||||||
@click.prevent="login"
|
@click.prevent="login"
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
type="submit"
|
type="submit"
|
||||||
>{{ $t("login.sign-in") }}</v-btn
|
>{{ $t("login.sign-in") }}</v-btn
|
||||||
>
|
>
|
||||||
</v-form>
|
</v-card-actions>
|
||||||
<v-alert v-if="error" outlined class="mt-3 mb-0" type="error">
|
<v-alert v-if="error" outlined class="mt-3 mb-0" type="error">
|
||||||
Could Not Validate Credentials
|
Could Not Validate Credentials
|
||||||
</v-alert>
|
</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-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-text>
|
<v-card-text v-if="startDate">
|
||||||
<MealPlanCard v-model="meals" />
|
<MealPlanCard v-model="meals" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-row align="center" justify="end">
|
<v-row align="center" justify="end">
|
||||||
|
@ -133,6 +133,7 @@ export default {
|
||||||
|
|
||||||
let dateDif = (endDate - startDate) / (1000 * 3600 * 24) + 1;
|
let dateDif = (endDate - startDate) / (1000 * 3600 * 24) + 1;
|
||||||
|
|
||||||
|
|
||||||
if (dateDif < 1) {
|
if (dateDif < 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -182,9 +183,9 @@ export default {
|
||||||
};
|
};
|
||||||
await api.mealPlans.create(mealBody);
|
await api.mealPlans.create(mealBody);
|
||||||
this.$emit("created");
|
this.$emit("created");
|
||||||
|
this.meals = [];
|
||||||
this.startDate = null;
|
this.startDate = null;
|
||||||
this.endDate = null;
|
this.endDate = null;
|
||||||
this.meals = [];
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getImage(image) {
|
getImage(image) {
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<v-card :to="`/recipe/${slug}`" max-height="125">
|
<v-card hover :to="`/recipe/${slug}`" max-height="125">
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<v-list-item-avatar rounded size="125" class="mt-0 ml-n4">
|
<v-list-item-avatar rounded size="125" class="mt-0 ml-n4">
|
||||||
<v-img :src="getImage(image)"> </v-img>
|
<v-img :src="getImage(image)"> </v-img>
|
||||||
</v-list-item-avatar>
|
</v-list-item-avatar>
|
||||||
<v-list-item-content>
|
<v-list-item-content class="align-self-start">
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
{{ name }}
|
{{ name }}
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
<v-rating length="5" size="16" dense :value="rating"></v-rating>
|
<v-rating length="5" size="16" dense :value="rating"></v-rating>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<v-list-item-action-text>
|
<v-list-item-action-text>
|
||||||
{{ description | truncate(50) }}
|
{{ description | truncate(115) }}
|
||||||
</v-list-item-action-text>
|
</v-list-item-action-text>
|
||||||
</div>
|
</div>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
|
@ -59,4 +59,8 @@ export default {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-top {
|
||||||
|
align-self: start !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -4,6 +4,7 @@
|
||||||
class="custom-transparent d-flex justify-start align-center text-center "
|
class="custom-transparent d-flex justify-start align-center text-center "
|
||||||
tile
|
tile
|
||||||
:width="`${timeCardWidth}`"
|
:width="`${timeCardWidth}`"
|
||||||
|
height="55"
|
||||||
v-if="totalTime || prepTime || performTime"
|
v-if="totalTime || prepTime || performTime"
|
||||||
>
|
>
|
||||||
<v-card flat color="rgb(255, 0, 0, 0.0)">
|
<v-card flat color="rgb(255, 0, 0, 0.0)">
|
||||||
|
|
|
@ -1,249 +1,56 @@
|
||||||
<template>
|
<template>
|
||||||
<v-data-table
|
<div>
|
||||||
:headers="headers"
|
<v-card flat>
|
||||||
:items="users"
|
<v-tabs
|
||||||
sort-by="calories"
|
v-model="tab"
|
||||||
class="elevation-1"
|
background-color="primary"
|
||||||
>
|
centered
|
||||||
<template v-slot:top>
|
dark
|
||||||
<v-toolbar flat>
|
icons-and-text
|
||||||
<v-toolbar-title>Mealie Users</v-toolbar-title>
|
>
|
||||||
<v-divider class="mx-4" inset vertical></v-divider>
|
<v-tabs-slider></v-tabs-slider>
|
||||||
<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>
|
|
||||||
|
|
||||||
<v-toolbar-title class="headline">
|
<v-tab>
|
||||||
{{ formTitle }}
|
Users
|
||||||
</v-toolbar-title>
|
<v-icon>mdi-account</v-icon>
|
||||||
|
</v-tab>
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
<v-tab>
|
||||||
<v-toolbar-title class="headline">
|
Sign-Up Links
|
||||||
User ID: {{ editedItem.id }}
|
<v-icon>mdi-account-plus-outline</v-icon>
|
||||||
</v-toolbar-title>
|
</v-tab>
|
||||||
</v-app-bar>
|
|
||||||
|
|
||||||
<v-card-text>
|
<v-tab>
|
||||||
<v-form ref="newUser">
|
Groups
|
||||||
<v-row>
|
<v-icon>mdi-account-group</v-icon>
|
||||||
<v-col cols="12" sm="12" md="6">
|
</v-tab>
|
||||||
<v-text-field
|
</v-tabs>
|
||||||
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-tabs-items v-model="tab" >
|
||||||
<v-spacer></v-spacer>
|
<v-tab-item>
|
||||||
<v-btn color="grey" text @click="close">
|
<TheUserTable />
|
||||||
Cancel
|
</v-tab-item>
|
||||||
</v-btn>
|
<v-tab-item>
|
||||||
<v-btn color="primary" @click="save">
|
<TheSignUpTable />
|
||||||
Save
|
</v-tab-item>
|
||||||
</v-btn>
|
<v-tab-item>
|
||||||
</v-card-actions>
|
<TheGroupTable />
|
||||||
</v-card>
|
</v-tab-item>
|
||||||
</v-dialog>
|
</v-tabs-items>
|
||||||
<Confirmation
|
</v-card>
|
||||||
ref="deleteUserDialog"
|
</div>
|
||||||
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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Confirmation from "@/components/UI/Confirmation";
|
import TheUserTable from "@/components/Admin/ManageUsers/TheUserTable";
|
||||||
import api from "@/api";
|
import TheGroupTable from "@/components/Admin/ManageUsers/TheGroupTable";
|
||||||
import { validators } from "@/mixins/validators";
|
import TheSignUpTable from "@/components/Admin/ManageUsers/TheSignUpTable";
|
||||||
export default {
|
export default {
|
||||||
components: { Confirmation },
|
components: { TheUserTable, TheGroupTable, TheSignUpTable },
|
||||||
mixins: [validators],
|
data() {
|
||||||
data: () => ({
|
return {
|
||||||
dialog: false,
|
tab: 0,
|
||||||
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>
|
</script>
|
||||||
|
|
|
@ -56,42 +56,6 @@
|
||||||
}}
|
}}
|
||||||
</p>
|
</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-row dense align-content="center" v-if="selectedTheme.colors">
|
||||||
<v-col>
|
<v-col>
|
||||||
<ColorPickerDialog
|
<ColorPickerDialog
|
||||||
|
@ -138,7 +102,28 @@
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</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>
|
<v-card-actions>
|
||||||
|
<NewThemeDialog @new-theme="appendTheme" class="mt-1" />
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn color="success" @click="saveThemes" class="mr-2">
|
<v-btn color="success" @click="saveThemes" class="mr-2">
|
||||||
<v-icon left> mdi-content-save </v-icon>
|
<v-icon left> mdi-content-save </v-icon>
|
||||||
|
@ -152,59 +137,35 @@
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import ColorPickerDialog from "@/components/Admin/Theme/ColorPickerDialog";
|
import ColorPickerDialog from "@/components/Admin/Theme/ColorPickerDialog";
|
||||||
import NewThemeDialog from "@/components/Admin/Theme/NewThemeDialog";
|
import NewThemeDialog from "@/components/Admin/Theme/NewThemeDialog";
|
||||||
import Confirmation from "@/components/UI/Confirmation";
|
import ThemeCard from "@/components/Admin/Theme/ThemeCard";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ColorPickerDialog,
|
ColorPickerDialog,
|
||||||
Confirmation,
|
|
||||||
NewThemeDialog,
|
NewThemeDialog,
|
||||||
|
ThemeCard,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedTheme: {},
|
|
||||||
selectedDarkMode: "system",
|
selectedDarkMode: "system",
|
||||||
availableThemes: [],
|
availableThemes: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.availableThemes = await api.themes.requestAll();
|
await this.getAllThemes();
|
||||||
this.selectedTheme = this.$store.getters.getActiveTheme;
|
|
||||||
this.selectedDarkMode = this.$store.getters.getDarkMode;
|
this.selectedDarkMode = this.$store.getters.getDarkMode;
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
computed: {
|
||||||
/**
|
selectedTheme() {
|
||||||
* Open the delete confirmation.
|
return this.$store.getters.getActiveTheme;
|
||||||
*/
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
/**
|
},
|
||||||
* 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();
|
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.
|
* Create the new Theme and select it.
|
||||||
|
@ -212,14 +173,8 @@ export default {
|
||||||
async appendTheme(NewThemeDialog) {
|
async appendTheme(NewThemeDialog) {
|
||||||
await api.themes.create(NewThemeDialog);
|
await api.themes.create(NewThemeDialog);
|
||||||
this.availableThemes.push(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() {
|
setStoresDarkMode() {
|
||||||
this.$store.commit("setDarkMode", this.selectedDarkMode);
|
this.$store.commit("setDarkMode", this.selectedDarkMode);
|
||||||
},
|
},
|
||||||
|
@ -227,13 +182,10 @@ export default {
|
||||||
* This will save the current colors and make the selected theme live.
|
* This will save the current colors and make the selected theme live.
|
||||||
*/
|
*/
|
||||||
async saveThemes() {
|
async saveThemes() {
|
||||||
if (this.$refs.form.validate()) {
|
await api.themes.update(
|
||||||
this.$store.commit("setTheme", this.selectedTheme);
|
this.selectedTheme.name,
|
||||||
await api.themes.update(
|
this.selectedTheme.colors
|
||||||
this.selectedTheme.name,
|
);
|
||||||
this.selectedTheme.colors
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-for="(meal, index) in mealplan.meals"
|
v-for="(meal, index) in mealplan.meals"
|
||||||
:key="generateKey(meal.slug, index)"
|
:key="generateKey(meal.slug, index)"
|
||||||
@click="$router.push(`/recipe/${meal.slug}`)"
|
:to="meal.slug ? `/recipe/${meal.slug}` : null"
|
||||||
>
|
>
|
||||||
<v-list-item-avatar
|
<v-list-item-avatar
|
||||||
color="primary"
|
color="primary"
|
||||||
|
@ -131,8 +131,8 @@ export default {
|
||||||
this.editMealPlan = null;
|
this.editMealPlan = null;
|
||||||
this.requestMeals();
|
this.requestMeals();
|
||||||
},
|
},
|
||||||
deletePlan(id) {
|
async deletePlan(id) {
|
||||||
api.mealPlans.delete(id);
|
await api.mealPlans.delete(id);
|
||||||
this.requestMeals();
|
this.requestMeals();
|
||||||
},
|
},
|
||||||
openShoppingList(id) {
|
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 MeaplPlanPage from "../pages/MealPlanPage";
|
||||||
import Debug from "../pages/Debug";
|
import Debug from "../pages/Debug";
|
||||||
import LoginPage from "../pages/LoginPage";
|
import LoginPage from "../pages/LoginPage";
|
||||||
|
import SignUpPage from "../pages/SignUpPage";
|
||||||
import MealPlanThisWeekPage from "../pages/MealPlanThisWeekPage";
|
import MealPlanThisWeekPage from "../pages/MealPlanThisWeekPage";
|
||||||
import api from "@/api";
|
import api from "@/api";
|
||||||
import Admin from "./admin";
|
import Admin from "./admin";
|
||||||
|
@ -25,6 +26,8 @@ export const routes = [
|
||||||
},
|
},
|
||||||
{ path: "/mealie", component: HomePage },
|
{ path: "/mealie", component: HomePage },
|
||||||
{ path: "/login", component: LoginPage },
|
{ path: "/login", component: LoginPage },
|
||||||
|
{ path: "/sign-up", redirect: "/" },
|
||||||
|
{ path: "/sign-up/:token", component: SignUpPage },
|
||||||
{ path: "/debug", component: Debug },
|
{ path: "/debug", component: Debug },
|
||||||
{ path: "/search", component: SearchPage },
|
{ path: "/search", component: SearchPage },
|
||||||
{ path: "/recipes/all", component: AllRecipesPage },
|
{ path: "/recipes/all", component: AllRecipesPage },
|
||||||
|
|
|
@ -4,6 +4,7 @@ from db.db_base import BaseDocument
|
||||||
from db.models.mealplan import MealPlanModel
|
from db.models.mealplan import MealPlanModel
|
||||||
from db.models.recipe import Category, RecipeModel, Tag
|
from db.models.recipe import Category, RecipeModel, Tag
|
||||||
from db.models.settings import SiteSettingsModel
|
from db.models.settings import SiteSettingsModel
|
||||||
|
from db.models.sign_up import SignUp
|
||||||
from db.models.theme import SiteThemeModel
|
from db.models.theme import SiteThemeModel
|
||||||
from db.models.users import User
|
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:
|
class Database:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.recipes = _Recipes()
|
self.recipes = _Recipes()
|
||||||
|
@ -80,6 +87,7 @@ class Database:
|
||||||
self.categories = _Categories()
|
self.categories = _Categories()
|
||||||
self.tags = _Tags()
|
self.tags = _Tags()
|
||||||
self.users = _Users()
|
self.users = _Users()
|
||||||
|
self.sign_ups = _SignUps()
|
||||||
|
|
||||||
|
|
||||||
db = Database()
|
db = Database()
|
||||||
|
|
|
@ -3,3 +3,4 @@ from db.models.recipe import *
|
||||||
from db.models.settings import *
|
from db.models.settings import *
|
||||||
from db.models.theme import *
|
from db.models.theme import *
|
||||||
from db.models.users 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
|
||||||
|
}
|
|
@ -20,4 +20,5 @@ def query_user(user_email: str, session: Session = None) -> UserInDB:
|
||||||
session = session if session else create_session()
|
session = session if session else create_session()
|
||||||
user = db.users.get(session, user_email, "email")
|
user = db.users.get(session, user_email, "email")
|
||||||
session.close()
|
session.close()
|
||||||
return UserInDB(**user)
|
return UserInDB(**user)
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ from typing import List
|
||||||
from db.database import db
|
from db.database import db
|
||||||
from db.db_setup import generate_session
|
from db.db_setup import generate_session
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from schema.snackbar import SnackResponse
|
||||||
from services.meal_services import MealPlan
|
from services.meal_services import MealPlan
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
from schema.snackbar import SnackResponse
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/meal-plans", tags=["Meal Plan"])
|
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 = [
|
ingredients = [
|
||||||
{"name": x.get("name"), "recipeIngredient": x.get("recipeIngredient")}
|
{"name": x.get("name"), "recipeIngredient": x.get("recipeIngredient")}
|
||||||
for x in recipes
|
for x in recipes
|
||||||
|
if x
|
||||||
]
|
]
|
||||||
|
|
||||||
return ingredients
|
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 fastapi import APIRouter
|
||||||
from routes.users import auth, crud
|
from routes.users import auth, crud, sign_up
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
router.include_router(sign_up.router)
|
||||||
router.include_router(auth.router)
|
router.include_router(auth.router)
|
||||||
router.include_router(crud.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"] = Cleaner.instructions(
|
||||||
recipe_data["recipeInstructions"]
|
recipe_data["recipeInstructions"]
|
||||||
)
|
)
|
||||||
recipe_data["image"] = Cleaner.image(recipe_data["image"])
|
recipe_data["image"] = Cleaner.image(recipe_data.get("image"))
|
||||||
recipe_data["slug"] = slugify(recipe_data["name"])
|
recipe_data["slug"] = slugify(recipe_data.get("name"))
|
||||||
recipe_data["orgURL"] = url
|
recipe_data["orgURL"] = url
|
||||||
|
|
||||||
return recipe_data
|
return recipe_data
|
||||||
|
@ -50,7 +50,9 @@ class Cleaner:
|
||||||
return cleantext
|
return cleantext
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def image(image) -> str:
|
def image(image=None) -> str:
|
||||||
|
if not image:
|
||||||
|
return "no image"
|
||||||
if type(image) == list:
|
if type(image) == list:
|
||||||
return image[0]
|
return image[0]
|
||||||
elif type(image) == dict:
|
elif type(image) == dict:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue