1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-05 13:35:23 +02:00

feat(frontend): Fix scheduler, forgot password flow, and minor bug fixes (#725)

* feat(frontend): 💄 add recipe title

* fix(frontend): 🐛 fixes #722 side-bar issue

* feat(frontend):  Add page titles to all pages

* minor cleanup

* refactor(backend): ♻️ rewrite scheduler to be more modulare and work

* feat(frontend):  start password reset functionality

* refactor(backend): ♻️ refactor application settings to facilitate dependency injection

* refactor(backend): 🔥 remove RECIPE_SETTINGS env variables in favor of group settings

* formatting

* refactor(backend): ♻️ align naming convention

* feat(backend):  password reset

* test(backend):  password reset

* feat(frontend):  self-service password reset

* purge password schedule

* update user creation for tests

Co-authored-by: Hayden <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-10-07 09:39:47 -08:00 committed by GitHub
parent d1f0441252
commit 2e9026f9ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
121 changed files with 1461 additions and 679 deletions

View file

@ -99,6 +99,11 @@ export default defineComponent({
appInfo,
};
},
head() {
return {
title: this.$t("about.about") as string,
};
},
});
</script>

View file

@ -146,6 +146,11 @@ export default defineComponent({
backupsFileNameDownload,
};
},
head() {
return {
title: this.$t("sidebar.backups") as string,
};
},
});
</script>

View file

@ -146,6 +146,11 @@ export default defineComponent({
return { statistics, events, deleteEvents, deleteEvent };
},
head() {
return {
title: this.$t("sidebar.dashboard") as string,
};
},
});
</script>

View file

@ -111,5 +111,10 @@ export default defineComponent({
return { ...toRefs(state), groups, refreshAllGroups, deleteGroup, createGroup };
},
head() {
return {
title: this.$t("group.manage-groups") as string,
};
},
});
</script>

View file

@ -153,6 +153,11 @@ export default defineComponent({
},
};
},
head() {
return {
title: this.$t("sidebar.manage-users") as string,
};
},
methods: {
updateUser(userData: any) {
this.updateMode = true;

View file

@ -17,6 +17,11 @@ export default defineComponent({
setup() {
return {};
},
head() {
return {
title: this.$t("settings.migrations") as string,
};
},
});
</script>

View file

@ -156,6 +156,11 @@ export default defineComponent({
testEmail,
};
},
head() {
return {
title: this.$t("settings.site-settings") as string,
};
},
});
</script>

View file

@ -12,6 +12,11 @@ export default defineComponent({
setup() {
return {};
},
head() {
return {
title: this.$t("sidebar.categories") as string,
};
},
});
</script>

View file

@ -113,6 +113,11 @@ export default defineComponent({
workingFoodData,
};
},
head() {
return {
title: "Foods",
};
},
});
</script>

View file

@ -215,6 +215,11 @@ export default defineComponent({
notificationTypes,
};
},
head() {
return {
title: this.$t("events.notification") as string,
};
},
});
</script>

View file

@ -12,6 +12,11 @@ export default defineComponent({
setup() {
return {};
},
head() {
return {
title: this.$t("settings.organize") as string,
};
},
});
</script>

View file

@ -12,6 +12,11 @@ export default defineComponent({
setup() {
return {};
},
head() {
return {
title: this.$t("sidebar.tags") as string,
};
},
});
</script>

View file

@ -115,6 +115,11 @@ export default defineComponent({
workingUnitData,
};
},
head() {
return {
title: "Units",
};
},
});
</script>

View file

@ -23,7 +23,7 @@
</template>
<script lang="ts">
import { defineComponent, useRoute, ref } from "@nuxtjs/composition-api";
import { defineComponent, useRoute, ref, useMeta } from "@nuxtjs/composition-api";
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
import { useCookbook } from "~/composables/use-group-cookbooks";
export default defineComponent({
@ -37,11 +37,18 @@ export default defineComponent({
const book = getOne(slug);
useMeta(() => {
return {
title: book?.value?.name || "Cookbook",
};
});
return {
book,
tab,
};
},
head: {}, // Must include for useMeta
});
</script>

View file

@ -0,0 +1,86 @@
<template>
<v-container fill-height fluid class="d-flex justify-center align-center">
<v-card color="background d-flex flex-column align-center" flat width="600px">
<v-card-title class="headline justify-center"> Forgot Password </v-card-title>
<BaseDivider />
<v-card-text>
<v-form @submit.prevent="requestLink()">
<v-text-field
v-model="email"
filled
rounded
autofocus
class="rounded-lg"
name="login"
:label="$t('user.email')"
type="text"
/>
<p class="text-center">Please enter your email address and we will send you a link to reset your password.</p>
<v-card-actions class="justify-center">
<div class="max-button">
<v-btn :loading="loading" color="primary" type="submit" large rounded class="rounded-xl" block>
<v-icon left>
{{ $globals.icons.email }}
</v-icon>
{{ $t("user.reset-password") }}
</v-btn>
</div>
</v-card-actions>
</v-form>
</v-card-text>
<v-btn class="mx-auto" text nuxt to="/login"> {{ $t("user.login") }} </v-btn>
</v-card>
</v-container>
</template>
<script lang="ts">
import { defineComponent, toRefs, reactive } from "@nuxtjs/composition-api";
import { useApiSingleton } from "~/composables/use-api";
import { alert } from "~/composables/use-toast";
export default defineComponent({
layout: "basic",
setup() {
const state = reactive({
email: "",
loading: false,
error: false,
});
const api = useApiSingleton();
async function requestLink() {
state.loading = true;
// TODO: Fix Response to send meaningful error
const { response } = await api.email.sendForgotPassword({ email: state.email });
if (response?.status === 200) {
state.loading = false;
state.error = false;
alert.success("Link successfully sent");
} else {
state.loading = false;
state.error = true;
alert.error("Email failure");
}
}
return {
requestLink,
...toRefs(state),
};
},
head() {
return {
title: this.$t("user.login") as string,
};
},
});
</script>
<style lang="css">
.max-button {
width: 300px;
}
</style>

View file

@ -18,9 +18,7 @@ export default defineComponent({
components: { RecipeCardSection },
setup() {
const { assignSorted } = useRecipes(false);
useStaticRoutes();
return { recentRecipes, assignSorted };
},
});

View file

@ -1,12 +1,12 @@
<template>
<v-container fill-height fluid class="d-flex justify-center align-center">
<v-card color="background d-flex flex-column align-center" flat width="600px">
<v-card tag="section" color="background d-flex flex-column align-center" flat width="600px">
<svg
id="bbc88faa-5a3b-49cf-bdbb-6c9ab11be594"
data-name="Layer 1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 728 754.88525"
style="max-height: 200px"
style="max-height: 100px"
class="mt-2"
>
<rect
@ -182,8 +182,11 @@
</v-card-actions>
</v-form>
</v-card-text>
<v-btn v-if="allowSignup" rounded class="mx-auto" text to="/register"> {{ $t("user.register") }} </v-btn>
<v-btn v-else class="mx-auto" text disabled> {{ $t("user.invite-only") }} </v-btn>
<v-card-actions>
<v-btn v-if="allowSignup" text to="/register"> {{ $t("user.register") }} </v-btn>
<v-btn v-else text disabled> {{ $t("user.invite-only") }} </v-btn>
<v-btn class="mr-auto" text to="/forgot-password"> {{ $t("user.reset-password") }} </v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
@ -223,10 +226,15 @@ export default defineComponent({
authenticate,
};
},
head() {
return {
title: this.$t("user.login") as string,
};
},
});
</script>
<style lang="css">
.max-button {
width: 300px;

View file

@ -309,6 +309,11 @@ export default defineComponent({
days,
};
},
head() {
return {
title: this.$t("meal-plan.dinner-this-week") as string,
};
},
});
</script>

View file

@ -9,6 +9,11 @@ export default defineComponent({
setup() {
return {};
},
head() {
return {
title: this.$t("meal-plan.dinner-this-week") as string,
};
},
});
</script>

View file

@ -397,6 +397,34 @@ export default defineComponent({
scale: 1,
});
// ===============================================================
// Metadata
const structuredData = computed(() => {
return {
"@context": "http://schema.org",
"@type": "Recipe",
...recipe.value,
};
});
useMeta(() => {
return {
title: recipe?.value?.name || "Recipe",
// @ts-ignore
mainImage: recipeImage(recipe?.value?.image),
meta: [
{
hid: "description",
name: "description",
content: recipe?.value?.description || "",
},
],
__dangerouslyDisableSanitizers: ["script"],
script: [{ innerHTML: JSON.stringify(structuredData), type: "application/ld+json" }],
};
});
return {
scaledYield,
...toRefs(state),

View file

@ -214,6 +214,11 @@ export default defineComponent({
validators,
};
},
head() {
return {
title: this.$t("general.create") as string,
};
},
// Computed State is used because of the limitation of vue-composition-api in v2.0
computed: {
tab: {

View file

@ -47,6 +47,11 @@ export default defineComponent({
return { recipes, infiniteScroll, loading };
},
head() {
return {
title: this.$t("page.all-recipes") as string,
};
},
});
</script>

View file

@ -29,6 +29,11 @@ export default defineComponent({
}, slug);
return { category };
},
head() {
return {
title: this.$t("sidebar.categories") as string,
};
},
methods: {
assignSorted(val: Array<Recipe>) {
if (this.category) {

View file

@ -59,6 +59,11 @@ export default defineComponent({
return { categories, api, categoriesByLetter };
},
head() {
return {
title: this.$t("sidebar.categories") as string,
};
},
});
</script>

View file

@ -29,6 +29,11 @@ export default defineComponent({
}, slug);
return { tag };
},
head() {
return {
title: this.$t("sidebar.tags") as string,
};
},
methods: {
assignSorted(val: Array<Recipe>) {
if (this.tag) {

View file

@ -59,6 +59,11 @@ export default defineComponent({
return { tags, api, tagsByLetter };
},
head() {
return {
title: this.$t("sidebar.tags") as string,
};
},
});
</script>

View file

@ -174,5 +174,10 @@ export default defineComponent({
register,
};
},
head() {
return {
title: this.$t("user.register") as string,
};
},
});
</script>

View file

@ -0,0 +1,140 @@
<template>
<v-container fill-height fluid class="d-flex justify-center align-center">
<v-card color="background d-flex flex-column align-center" flat width="600px">
<v-card-title class="headline justify-center"> Reset Password </v-card-title>
<BaseDivider />
<v-card-text>
<v-form @submit.prevent="requestLink()">
<v-text-field
v-model="email"
:prepend-icon="$globals.icons.email"
filled
rounded
autofocus
class="rounded-lg"
name="login"
:label="$t('user.email')"
type="text"
/>
<v-text-field
v-model="password"
filled
rounded
class="rounded-lg"
:prepend-icon="$globals.icons.lock"
name="password"
label="Password"
type="password"
:rules="[validators.required]"
/>
<v-text-field
v-model="passwordConfirm"
filled
rounded
validate-on-blur
class="rounded-lg"
:prepend-icon="$globals.icons.lock"
name="password"
label="Confirm Password"
type="password"
:rules="[validators.required, passwordMatch]"
/>
<p class="text-center">Please enter your new password.</p>
<v-card-actions class="justify-center">
<div class="max-button">
<v-btn
:loading="loading"
color="primary"
:disabled="token === ''"
type="submit"
large
rounded
class="rounded-xl"
block
>
<v-icon left>
{{ $globals.icons.lock }}
</v-icon>
{{ token === "" ? "Token Required" : $t("user.reset-password") }}
</v-btn>
</div>
</v-card-actions>
</v-form>
</v-card-text>
<v-btn class="mx-auto" text nuxt to="/login"> {{ $t("user.login") }} </v-btn>
</v-card>
</v-container>
</template>
<script lang="ts">
import { defineComponent, toRefs, reactive } from "@nuxtjs/composition-api";
import { useApiSingleton } from "~/composables/use-api";
import { alert } from "~/composables/use-toast";
import { validators } from "@/composables/use-validators";
import { useRouteQuery } from "~/composables/use-router";
export default defineComponent({
layout: "basic",
setup() {
const state = reactive({
email: "",
password: "",
passwordConfirm: "",
loading: false,
error: false,
});
const passwordMatch = () => state.password === state.passwordConfirm || "Passwords do not match";
// ===================
// Token Getter
const token = useRouteQuery("token", "");
// ===================
// API
const api = useApiSingleton();
async function requestLink() {
state.loading = true;
// TODO: Fix Response to send meaningful error
const { response } = await api.users.resetPassword({
token: token.value,
email: state.email,
password: state.password,
passwordConfirm: state.passwordConfirm,
});
state.loading = false;
if (response?.status === 200) {
state.loading = false;
state.error = false;
alert.success("Password Reset Successful");
} else {
state.loading = false;
state.error = true;
alert.error("Something Went Wrong");
}
}
return {
passwordMatch,
token,
requestLink,
validators,
...toRefs(state),
};
},
head() {
return {
title: this.$t("user.login") as string,
};
},
});
</script>
<style lang="css">
.max-button {
width: 300px;
}
</style>

View file

@ -110,6 +110,11 @@ export default defineComponent({
},
};
},
head() {
return {
title: this.$t("search.search"),
};
},
computed: {
searchString: {
set(q) {

View file

@ -9,6 +9,11 @@ export default defineComponent({
setup() {
return {};
},
head() {
return {
title: this.$t("shopping-list.shopping-list") as string,
};
},
});
</script>

View file

@ -1,16 +1,21 @@
<template>
<div></div>
</template>
<div></div>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
setup() {
return {};
},
head() {
return {
title: this.$t("shopping-list.shopping-list") as string,
};
},
});
</script>
export default defineComponent({
setup() {
return {}
}
})
</script>
<style scoped>
</style>
<style scoped>
</style>

View file

@ -9,6 +9,11 @@ export default defineComponent({
setup() {
return {};
},
head() {
return {
title: this.$t("general.favorites") as string,
};
},
});
</script>

View file

@ -3,13 +3,18 @@
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
setup() {
return {}
}
})
return {};
},
head() {
return {
title: this.$t("settings.profile") as string,
};
},
});
</script>
<style scoped>

View file

@ -63,6 +63,11 @@ export default defineComponent({
actions,
};
},
head() {
return {
title: this.$t("settings.pages") as string,
};
},
});
</script>

View file

@ -133,6 +133,11 @@ export default defineComponent({
allDays,
};
},
head() {
return {
title: this.$t("group.group") as string,
};
},
});
</script>

View file

@ -109,5 +109,10 @@ export default defineComponent({
return { members, headers, setPermissions };
},
head() {
return {
title: "Members",
};
},
});
</script>

View file

@ -64,5 +64,10 @@ export default defineComponent({
actions,
};
},
head() {
return {
title: this.$t("settings.webhooks.webhooks") as string,
};
},
});
</script>

View file

@ -126,6 +126,11 @@ export default defineComponent({
return { createToken, deleteToken, copyToken, createdToken, loading, name, user, resetCreate };
},
head() {
return {
title: this.$t("settings.token.api-tokens") as string,
};
},
});
</script>

View file

@ -7,8 +7,6 @@
<template #title> Your Profile Settings </template>
</BasePageTitle>
<section>
<ToggleState tag="article">
<template #activator="{ toggle, state }">
@ -161,6 +159,11 @@ export default defineComponent({
loading: false,
};
},
head() {
return {
title: this.$t("settings.profile") as string,
};
},
methods: {
async changePassword() {

View file

@ -193,6 +193,11 @@ export default defineComponent({
...toRefs(state),
};
},
head() {
return {
title: this.$t("settings.profile") as string,
};
},
});
</script>

View file

@ -1,16 +0,0 @@
<template>
<div></div>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
setup() {
return {};
},
});
</script>
<style scoped>
</style>

View file

@ -1,113 +0,0 @@
<template>
<v-container fill-height fluid class="d-flex justify-center align-start">
<v-card color="background d-flex flex-column align-center " flat width="600px">
<svg
id="b76bd6b3-ad77-41ff-b778-1d1d054fe577"
data-name="Layer 1"
xmlns="http://www.w3.org/2000/svg"
style="max-height: 300px"
viewBox="0 0 570 511.67482"
>
<path
d="M879.99927,389.83741a.99678.99678,0,0,1-.5708-.1792L602.86963,197.05469a5.01548,5.01548,0,0,0-5.72852.00977L322.57434,389.65626a1.00019,1.00019,0,0,1-1.14868-1.6377l274.567-192.5918a7.02216,7.02216,0,0,1,8.02-.01318l276.55883,192.603a1.00019,1.00019,0,0,1-.57226,1.8208Z"
transform="translate(-315 -194.16259)"
fill="#3f3d56"
/>
<polygon
points="23.264 202.502 285.276 8.319 549.276 216.319 298.776 364.819 162.776 333.819 23.264 202.502"
fill="#e6e6e6"
/>
<path
d="M489.25553,650.70367H359.81522a6.04737,6.04737,0,1,1,0-12.09473H489.25553a6.04737,6.04737,0,1,1,0,12.09473Z"
transform="translate(-315 -194.16259)"
fill="#e58325"
/>
<path
d="M406.25553,624.70367H359.81522a6.04737,6.04737,0,1,1,0-12.09473h46.44031a6.04737,6.04737,0,1,1,0,12.09473Z"
transform="translate(-315 -194.16259)"
fill="#e58325"
/>
<path
d="M603.96016,504.82207a7.56366,7.56366,0,0,1-2.86914-.562L439.5002,437.21123v-209.874a7.00817,7.00817,0,0,1,7-7h310a7.00818,7.00818,0,0,1,7,7v210.0205l-.30371.12989L606.91622,504.22734A7.61624,7.61624,0,0,1,603.96016,504.82207Z"
transform="translate(-315 -194.16259)"
fill="#fff"
/>
<path
d="M603.96016,505.32158a8.07177,8.07177,0,0,1-3.05957-.59863L439.0002,437.54521v-210.208a7.50851,7.50851,0,0,1,7.5-7.5h310a7.50851,7.50851,0,0,1,7.5,7.5V437.68779l-156.8877,66.999A8.10957,8.10957,0,0,1,603.96016,505.32158Zm-162.96-69.1123,160.66309,66.66455a6.1182,6.1182,0,0,0,4.668-.02784l155.669-66.47851V227.33721a5.50653,5.50653,0,0,0-5.5-5.5h-310a5.50653,5.50653,0,0,0-5.5,5.5Z"
transform="translate(-315 -194.16259)"
fill="#3f3d56"
/>
<path
d="M878,387.83741h-.2002L763,436.85743l-157.06982,67.07a5.06614,5.06614,0,0,1-3.88038.02L440,436.71741l-117.62012-48.8-.17968-.08H322a7.00778,7.00778,0,0,0-7,7v304a7.00779,7.00779,0,0,0,7,7H878a7.00779,7.00779,0,0,0,7-7v-304A7.00778,7.00778,0,0,0,878,387.83741Zm5,311a5.002,5.002,0,0,1-5,5H322a5.002,5.002,0,0,1-5-5v-304a5.01106,5.01106,0,0,1,4.81006-5L440,438.87739l161.28027,66.92a7.12081,7.12081,0,0,0,5.43994-.03L763,439.02741l115.2002-49.19a5.01621,5.01621,0,0,1,4.7998,5Z"
transform="translate(-315 -194.16259)"
fill="#3f3d56"
/>
<path
d="M602.345,445.30958a27.49862,27.49862,0,0,1-16.5459-5.4961l-.2959-.22217-62.311-47.70752a27.68337,27.68337,0,1,1,33.67407-43.94921l40.36035,30.94775,95.37793-124.38672a27.68235,27.68235,0,0,1,38.81323-5.12353l-.593.80517.6084-.79346a27.71447,27.71447,0,0,1,5.12353,38.81348L624.36938,434.50586A27.69447,27.69447,0,0,1,602.345,445.30958Z"
transform="translate(-315 -194.16259)"
fill="#e58325"
/>
</svg>
<v-card-title class="headline justify-center"> Request Secure Link </v-card-title>
<BaseDivider />
<v-card-text>
<v-form @submit.prevent="authenticate()">
<v-text-field
v-model="form.email"
filled
rounded
autofocus
class="rounded-lg"
prepend-icon="mdi-account"
name="login"
label="Email"
type="text"
/>
<v-btn :loading="loggingIn" color="primary" type="submit" large rounded class="rounded-xl" block>
Submit
</v-btn>
</v-form>
</v-card-text>
<v-btn v-if="allowSignup" class="mx-auto" text to="/login"> Login </v-btn>
</v-card>
<!-- <v-col class="fill-height"> </v-col> -->
</v-container>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
layout: "basic",
setup() {
return {};
},
data() {
return {
loggingIn: false,
form: {
email: "changeme@email.com",
password: "MyPassword",
},
};
},
computed: {
allowSignup(): boolean {
// @ts-ignore
return process.env.ALLOW_SIGNUP;
},
},
methods: {
async authenticate() {
this.loggingIn = true;
const formData = new FormData();
formData.append("username", this.form.email);
formData.append("password", this.form.password);
await this.$auth.loginWith("local", { data: formData });
this.loggingIn = false;
},
},
});
</script>