mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-11-30 07:34:10 -05:00
feat: Remove backend cookie and use frontend for auth (#6601)
This commit is contained in:
@@ -4,22 +4,22 @@
|
|||||||
|
|
||||||
### General
|
### General
|
||||||
|
|
||||||
| Variables | Default | Description |
|
| Variables | Default | Description |
|
||||||
| ----------------------------- | :-------------------: | -------------------------------------------------------------------------------------------------- |
|
| ----------------------------- | :-------------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| PUID | 911 | UserID permissions between host OS and container |
|
| PUID | 911 | UserID permissions between host OS and container |
|
||||||
| PGID | 911 | GroupID permissions between host OS and container |
|
| PGID | 911 | GroupID permissions between host OS and container |
|
||||||
| DEFAULT_GROUP | Home | The default group for users |
|
| DEFAULT_GROUP | Home | The default group for users |
|
||||||
| DEFAULT_HOUSEHOLD | Family | The default household for users in each group |
|
| DEFAULT_HOUSEHOLD | Family | The default household for users in each group |
|
||||||
| BASE_URL | http://localhost:8080 | Used for Notifications |
|
| BASE_URL | http://localhost:8080 | Used for Notifications |
|
||||||
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid. Must be <= 87600 (10 years, in hours). |
|
| TOKEN_TIME | 48 | The time in hours that a login/auth token is valid. Must be <= 9600 (400 days, in hours). |
|
||||||
| API_PORT | 9000 | The port exposed by backend API. **Do not change this if you're running in Docker** |
|
| API_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 | Must be set to get correct date/time on the server |
|
| TZ | UTC | Must be set to get correct date/time on the server |
|
||||||
| ALLOW_SIGNUP<super>\*</super> | false | Allow user sign-up without token |
|
| ALLOW_SIGNUP<super>\*</super> | false | Allow user sign-up without token |
|
||||||
| ALLOW_PASSWORD_LOGIN | true | Whether or not to display the username+password input fields. Keep set to true unless you use OIDC authentication |
|
| ALLOW_PASSWORD_LOGIN | true | Whether or not to display the username+password input fields. Keep set to true unless you use OIDC authentication |
|
||||||
| LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path |
|
| LOG_CONFIG_OVERRIDE | | Override the config for logging with a custom path |
|
||||||
| LOG_LEVEL | info | Logging level (e.g. critical, error, warning, info, debug) |
|
| LOG_LEVEL | info | Logging level (e.g. critical, error, warning, info, debug) |
|
||||||
| DAILY_SCHEDULE_TIME | 23:45 | The time of day to run daily server tasks, in HH:MM format. Use the server's local time, *not* UTC |
|
| DAILY_SCHEDULE_TIME | 23:45 | The time of day to run daily server tasks, in HH:MM format. Use the server's local time, *not* UTC |
|
||||||
|
|
||||||
<super>\*</super> Starting in v1.4.0 this was changed to default to `false` as part of a security review of the application.
|
<super>\*</super> Starting in v1.4.0 this was changed to default to `false` as part of a security review of the application.
|
||||||
|
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ import { VueDraggable } from "vue-draggable-plus";
|
|||||||
import type { IngredientFood, IngredientUnit, ParsedIngredient, RecipeIngredient } from "~/lib/api/types/recipe";
|
import type { IngredientFood, IngredientUnit, ParsedIngredient, RecipeIngredient } from "~/lib/api/types/recipe";
|
||||||
import type { Parser } from "~/lib/api/user/recipes/recipe";
|
import type { Parser } from "~/lib/api/user/recipes/recipe";
|
||||||
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
import type { NoUndefinedField } from "~/lib/api/types/non-generated";
|
||||||
import { useAppInfo, useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { parseIngredientText } from "~/composables/recipes";
|
import { parseIngredientText } from "~/composables/recipes";
|
||||||
import { useFoodData, useFoodStore, useUnitData, useUnitStore } from "~/composables/store";
|
import { useFoodData, useFoodStore, useUnitData, useUnitStore } from "~/composables/store";
|
||||||
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
||||||
@@ -213,9 +213,9 @@ const emit = defineEmits<{
|
|||||||
(e: "save", value: NoUndefinedField<RecipeIngredient[]>): void;
|
(e: "save", value: NoUndefinedField<RecipeIngredient[]>): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const { $appInfo } = useNuxtApp();
|
||||||
const i18n = useGlobalI18n();
|
const i18n = useGlobalI18n();
|
||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const appInfo = useAppInfo();
|
|
||||||
const drag = ref(false);
|
const drag = ref(false);
|
||||||
|
|
||||||
const unitStore = useUnitStore();
|
const unitStore = useUnitStore();
|
||||||
@@ -238,7 +238,7 @@ const availableParsers = computed(() => {
|
|||||||
{
|
{
|
||||||
text: i18n.t("recipe.parser.openai-parser"),
|
text: i18n.t("recipe.parser.openai-parser"),
|
||||||
value: "openai",
|
value: "openai",
|
||||||
hide: !appInfo.value?.enableOpenai,
|
hide: !$appInfo.enableOpenai,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -97,7 +97,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import type { SideBarLink } from "~/types/application-types";
|
import type { SideBarLink } from "~/types/application-types";
|
||||||
import { useAppInfo } from "~/composables/api";
|
|
||||||
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
import { useCookbookPreferences } from "~/composables/use-users/preferences";
|
||||||
import { useCookbookStore, usePublicCookbookStore } from "~/composables/store/use-cookbook-store";
|
import { useCookbookStore, usePublicCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||||
@@ -105,7 +104,7 @@ import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
|||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const { $globals } = useNuxtApp();
|
const { $appInfo, $globals } = useNuxtApp();
|
||||||
const display = useDisplay();
|
const display = useDisplay();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const { isOwnGroup } = useLoggedInState();
|
const { isOwnGroup } = useLoggedInState();
|
||||||
@@ -135,9 +134,7 @@ export default defineNuxtComponent({
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const appInfo = useAppInfo();
|
const showImageImport = computed(() => $appInfo.enableOpenaiImageServices);
|
||||||
const showImageImport = computed(() => appInfo.value?.enableOpenaiImageServices);
|
|
||||||
|
|
||||||
const languageDialog = ref<boolean>(false);
|
const languageDialog = ref<boolean>(false);
|
||||||
|
|
||||||
const sidebar = ref<boolean>(false);
|
const sidebar = ref<boolean>(false);
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
export { useAppInfo } from "./use-app-info";
|
|
||||||
export { useStaticRoutes } from "./static-routes";
|
export { useStaticRoutes } from "./static-routes";
|
||||||
export { useAdminApi, usePublicApi, usePublicExploreApi, useUserApi } from "./api-client";
|
export { useAdminApi, usePublicApi, usePublicExploreApi, useUserApi } from "./api-client";
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import type { AppInfo } from "~/lib/api/types/admin";
|
|
||||||
|
|
||||||
export function useAppInfo(): Ref<AppInfo | null> {
|
|
||||||
const i18n = useI18n();
|
|
||||||
const { $axios } = useNuxtApp();
|
|
||||||
$axios.defaults.headers.common["Accept-Language"] = i18n.locale.value;
|
|
||||||
|
|
||||||
const { data: appInfo } = useAsyncData("app-info", async () => {
|
|
||||||
const data = await $axios.get<AppInfo>("/api/app/about");
|
|
||||||
return data.data;
|
|
||||||
});
|
|
||||||
|
|
||||||
return appInfo;
|
|
||||||
}
|
|
||||||
@@ -5,9 +5,9 @@ const userRatings = ref<UserRatingSummary[]>([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const ready = ref(false);
|
const ready = ref(false);
|
||||||
|
|
||||||
const $auth = useMealieAuth();
|
|
||||||
|
|
||||||
export const useUserSelfRatings = function () {
|
export const useUserSelfRatings = function () {
|
||||||
|
const $auth = useMealieAuth();
|
||||||
|
|
||||||
async function refreshUserRatings() {
|
async function refreshUserRatings() {
|
||||||
if (!$auth.user.value || loading.value) {
|
if (!$auth.user.value || loading.value) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -23,13 +23,15 @@ const authUser = ref<UserOut | null>(null);
|
|||||||
const authStatus = ref<"loading" | "authenticated" | "unauthenticated">("loading");
|
const authStatus = ref<"loading" | "authenticated" | "unauthenticated">("loading");
|
||||||
|
|
||||||
export const useAuthBackend = function (): AuthState {
|
export const useAuthBackend = function (): AuthState {
|
||||||
const { $axios } = useNuxtApp();
|
const { $appInfo, $axios } = useNuxtApp();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const runtimeConfig = useRuntimeConfig();
|
const runtimeConfig = useRuntimeConfig();
|
||||||
const tokenTimeHours = Number(runtimeConfig.public.TOKEN_TIME) || 48;
|
|
||||||
const tokenName = runtimeConfig.public.AUTH_TOKEN;
|
const tokenName = runtimeConfig.public.AUTH_TOKEN;
|
||||||
const tokenCookie = useCookie(tokenName, { maxAge: tokenTimeHours * 60 * 60 });
|
const tokenCookie = useCookie(tokenName, {
|
||||||
|
maxAge: $appInfo.tokenTime * 60 * 60,
|
||||||
|
secure: $appInfo.production && window?.location?.protocol === "https:",
|
||||||
|
});
|
||||||
|
|
||||||
function setToken(token: string | null) {
|
function setToken(token: string | null) {
|
||||||
tokenCookie.value = token;
|
tokenCookie.value = token;
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
<TheSnackbar />
|
<TheSnackbar />
|
||||||
|
|
||||||
<v-banner
|
<v-banner
|
||||||
v-if="isDemo"
|
v-if="$appInfo.demoStatus"
|
||||||
sticky
|
sticky
|
||||||
>
|
>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<b> {{ $t("demo.info_message_with_version", { version: version }) }} </b>
|
<b> {{ $t("demo.info_message_with_version", { version: $appInfo.version }) }} </b>
|
||||||
</div>
|
</div>
|
||||||
</v-banner>
|
</v-banner>
|
||||||
|
|
||||||
@@ -23,24 +23,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TheSnackbar from "~/components/Layout/LayoutParts/TheSnackbar.vue";
|
import TheSnackbar from "~/components/Layout/LayoutParts/TheSnackbar.vue";
|
||||||
import { useAppInfo } from "~/composables/api";
|
|
||||||
import { useGlobalI18n } from "~/composables/use-global-i18n";
|
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
components: { TheSnackbar },
|
components: { TheSnackbar },
|
||||||
setup() {
|
|
||||||
const appInfo = useAppInfo();
|
|
||||||
|
|
||||||
const isDemo = computed(() => appInfo?.value?.demoStatus || false);
|
|
||||||
|
|
||||||
const i18n = useGlobalI18n();
|
|
||||||
const version = computed(() => appInfo?.value?.version || i18n.t("about.unknown-version"));
|
|
||||||
|
|
||||||
return {
|
|
||||||
appInfo,
|
|
||||||
isDemo,
|
|
||||||
version,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface AdminAboutInfo {
|
|||||||
oidcProviderName: string;
|
oidcProviderName: string;
|
||||||
enableOpenai: boolean;
|
enableOpenai: boolean;
|
||||||
enableOpenaiImageServices: boolean;
|
enableOpenaiImageServices: boolean;
|
||||||
|
tokenTime: number;
|
||||||
versionLatest: string;
|
versionLatest: string;
|
||||||
apiPort: number;
|
apiPort: number;
|
||||||
apiDocs: boolean;
|
apiDocs: boolean;
|
||||||
@@ -50,6 +51,7 @@ export interface AppInfo {
|
|||||||
oidcProviderName: string;
|
oidcProviderName: string;
|
||||||
enableOpenai: boolean;
|
enableOpenai: boolean;
|
||||||
enableOpenaiImageServices: boolean;
|
enableOpenaiImageServices: boolean;
|
||||||
|
tokenTime: number;
|
||||||
}
|
}
|
||||||
export interface AppStartupInfo {
|
export interface AppStartupInfo {
|
||||||
isFirstLogin: boolean;
|
isFirstLogin: boolean;
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ export default defineNuxtConfig({
|
|||||||
apiUrl: process.env.API_URL || "http://localhost:9000",
|
apiUrl: process.env.API_URL || "http://localhost:9000",
|
||||||
public: {
|
public: {
|
||||||
AUTH_TOKEN,
|
AUTH_TOKEN,
|
||||||
TOKEN_TIME: process.env.TOKEN_TIME || "48",
|
|
||||||
GLOBAL_MIDDLEWARE: process.env.GLOBAL_MIDDLEWARE || undefined,
|
GLOBAL_MIDDLEWARE: process.env.GLOBAL_MIDDLEWARE || undefined,
|
||||||
SUB_PATH: process.env.SUB_PATH || "",
|
SUB_PATH: process.env.SUB_PATH || "",
|
||||||
// ==============================================
|
// ==============================================
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useAppInfo } from "~/composables/api";
|
|
||||||
import type { MenuItem } from "~/components/global/BaseOverflowButton.vue";
|
import type { MenuItem } from "~/components/global/BaseOverflowButton.vue";
|
||||||
import AdvancedOnly from "~/components/global/AdvancedOnly.vue";
|
import AdvancedOnly from "~/components/global/AdvancedOnly.vue";
|
||||||
|
|
||||||
@@ -53,15 +52,12 @@ export default defineNuxtComponent({
|
|||||||
setup() {
|
setup() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const $globals = useNuxtApp().$globals;
|
const { $appInfo, $globals } = useNuxtApp();
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: i18n.t("general.create"),
|
title: i18n.t("general.create"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const appInfo = useAppInfo();
|
|
||||||
const enableOpenAIImages = computed(() => appInfo.value?.enableOpenaiImageServices);
|
|
||||||
|
|
||||||
const subpages = computed<MenuItem[]>(() => [
|
const subpages = computed<MenuItem[]>(() => [
|
||||||
{
|
{
|
||||||
icon: $globals.icons.link,
|
icon: $globals.icons.link,
|
||||||
@@ -82,7 +78,7 @@ export default defineNuxtComponent({
|
|||||||
icon: $globals.icons.fileImage,
|
icon: $globals.icons.fileImage,
|
||||||
text: i18n.t("recipe.create-from-images"),
|
text: i18n.t("recipe.create-from-images"),
|
||||||
value: "image",
|
value: "image",
|
||||||
hide: !enableOpenAIImages.value,
|
hide: !$appInfo.enableOpenaiImageServices,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: $globals.icons.edit,
|
icon: $globals.icons.edit,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-text v-if="appInfo && appInfo.enableOpenai">
|
<v-card-text v-if="$appInfo.enableOpenai">
|
||||||
{{ $t('recipe.recipe-debugger-use-openai-description') }}
|
{{ $t('recipe.recipe-debugger-use-openai-description') }}
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-model="useOpenAI"
|
v-model="useOpenAI"
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useAppInfo, useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { validators } from "~/composables/use-validators";
|
import { validators } from "~/composables/use-validators";
|
||||||
import type { Recipe } from "~/lib/api/types/recipe";
|
import type { Recipe } from "~/lib/api/types/recipe";
|
||||||
|
|
||||||
@@ -83,7 +83,6 @@ export default defineNuxtComponent({
|
|||||||
const api = useUserApi();
|
const api = useUserApi();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const appInfo = useAppInfo();
|
|
||||||
|
|
||||||
const recipeUrl = computed({
|
const recipeUrl = computed({
|
||||||
set(recipe_import_url: string | null) {
|
set(recipe_import_url: string | null) {
|
||||||
@@ -115,7 +114,6 @@ export default defineNuxtComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appInfo,
|
|
||||||
recipeUrl,
|
recipeUrl,
|
||||||
debugTreeView,
|
debugTreeView,
|
||||||
debugUrl,
|
debugUrl,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
<v-card-text class="w-100">
|
<v-card-text class="w-100">
|
||||||
<v-form @submit.prevent="authenticate">
|
<v-form @submit.prevent="authenticate">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-if="allowPasswordLogin"
|
v-if="$appInfo.allowPasswordLogin"
|
||||||
v-model="form.email"
|
v-model="form.email"
|
||||||
:prepend-inner-icon="$globals.icons.email"
|
:prepend-inner-icon="$globals.icons.email"
|
||||||
variant="solo-filled"
|
variant="solo-filled"
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-if="allowPasswordLogin"
|
v-if="$appInfo.allowPasswordLogin"
|
||||||
id="password"
|
id="password"
|
||||||
v-model="form.password"
|
v-model="form.password"
|
||||||
:prepend-inner-icon="$globals.icons.lock"
|
:prepend-inner-icon="$globals.icons.lock"
|
||||||
@@ -86,12 +86,12 @@
|
|||||||
@click:append-inner="togglePasswordShow"
|
@click:append-inner="togglePasswordShow"
|
||||||
/>
|
/>
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
v-if="allowPasswordLogin"
|
v-if="$appInfo.allowPasswordLogin"
|
||||||
v-model="form.remember"
|
v-model="form.remember"
|
||||||
class="ml-2 mt-n2"
|
class="ml-2 mt-n2"
|
||||||
:label="$t('user.remember-me')"
|
:label="$t('user.remember-me')"
|
||||||
/>
|
/>
|
||||||
<v-card-actions v-if="allowPasswordLogin" class="justify-center pt-0">
|
<v-card-actions v-if="$appInfo.allowPasswordLogin" class="justify-center pt-0">
|
||||||
<div class="max-button">
|
<div class="max-button">
|
||||||
<v-btn
|
<v-btn
|
||||||
:loading="loggingIn"
|
:loading="loggingIn"
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="appInfoLoaded && allowOidc && allowPasswordLogin"
|
v-if="$appInfo.enableOidc && $appInfo.allowPasswordLogin"
|
||||||
class="d-flex my-4 justify-center align-center"
|
class="d-flex my-4 justify-center align-center"
|
||||||
width="80%"
|
width="80%"
|
||||||
>
|
>
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<v-card-actions
|
<v-card-actions
|
||||||
v-if="appInfoLoaded && allowOidc"
|
v-if="$appInfo.enableOidc"
|
||||||
class="justify-center"
|
class="justify-center"
|
||||||
>
|
>
|
||||||
<div class="max-button">
|
<div class="max-button">
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
block
|
block
|
||||||
@click="() => oidcAuthenticate()"
|
@click="() => oidcAuthenticate()"
|
||||||
>
|
>
|
||||||
{{ $t("user.login-oidc") }} {{ oidcProviderName }}
|
{{ $t("user.login-oidc") }} {{ $appInfo.oidcProviderName }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions class="d-flex justify-center flex-column flex-sm-row">
|
<v-card-actions class="d-flex justify-center flex-column flex-sm-row">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="allowSignup && allowPasswordLogin"
|
v-if="$appInfo.allowSignup && $appInfo.allowPasswordLogin"
|
||||||
variant="text"
|
variant="text"
|
||||||
to="/register"
|
to="/register"
|
||||||
>
|
>
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
{{ $t("user.invite-only") }}
|
{{ $t("user.invite-only") }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="allowPasswordLogin"
|
v-if="$appInfo.allowPasswordLogin"
|
||||||
class="mr-auto"
|
class="mr-auto"
|
||||||
variant="text"
|
variant="text"
|
||||||
to="/forgot-password"
|
to="/forgot-password"
|
||||||
@@ -212,7 +212,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useDark, whenever } from "@vueuse/core";
|
import { useDark, whenever } from "@vueuse/core";
|
||||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import { useAppInfo } from "~/composables/api";
|
|
||||||
import { usePasswordField } from "~/composables/use-passwords";
|
import { usePasswordField } from "~/composables/use-passwords";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
import { useAsyncKey } from "~/composables/use-utils";
|
import { useAsyncKey } from "~/composables/use-utils";
|
||||||
@@ -229,7 +228,7 @@ export default defineNuxtComponent({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const $auth = useMealieAuth();
|
const $auth = useMealieAuth();
|
||||||
const { $axios } = useNuxtApp();
|
const { $appInfo, $axios } = useNuxtApp();
|
||||||
const { loggedIn } = useLoggedInState();
|
const { loggedIn } = useLoggedInState();
|
||||||
const groupSlug = computed(() => $auth.user.value?.groupSlug);
|
const groupSlug = computed(() => $auth.user.value?.groupSlug);
|
||||||
const isDemo = ref(false);
|
const isDemo = ref(false);
|
||||||
@@ -276,19 +275,10 @@ export default defineNuxtComponent({
|
|||||||
const loggingIn = ref(false);
|
const loggingIn = ref(false);
|
||||||
const oidcLoggingIn = ref(false);
|
const oidcLoggingIn = ref(false);
|
||||||
|
|
||||||
const appInfo = useAppInfo();
|
|
||||||
|
|
||||||
const { passwordIcon, inputType, togglePasswordShow } = usePasswordField();
|
const { passwordIcon, inputType, togglePasswordShow } = usePasswordField();
|
||||||
|
|
||||||
const appInfoLoaded = computed(() => appInfo.value !== null);
|
|
||||||
const allowSignup = computed(() => appInfo.value?.allowSignup || false);
|
|
||||||
const allowOidc = computed(() => appInfo.value?.enableOidc || false);
|
|
||||||
const oidcRedirect = computed(() => appInfo.value?.oidcRedirect || false);
|
|
||||||
const oidcProviderName = computed(() => appInfo.value?.oidcProviderName || "OAuth");
|
|
||||||
const allowPasswordLogin = computed(() => appInfo.value?.allowPasswordLogin ?? true);
|
|
||||||
|
|
||||||
whenever(
|
whenever(
|
||||||
() => appInfoLoaded.value && allowOidc.value && oidcRedirect.value && !isCallback() && !isDirectLogin() /* && !$auth.check().valid */,
|
() => $appInfo.enableOidc && $appInfo.oidcRedirect && !isCallback() && !isDirectLogin() /* && !$auth.check().valid */,
|
||||||
() => oidcAuthenticate(),
|
() => oidcAuthenticate(),
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
@@ -368,13 +358,8 @@ export default defineNuxtComponent({
|
|||||||
isDark,
|
isDark,
|
||||||
form,
|
form,
|
||||||
loggingIn,
|
loggingIn,
|
||||||
appInfoLoaded,
|
|
||||||
allowSignup,
|
|
||||||
allowPasswordLogin,
|
|
||||||
allowOidc,
|
|
||||||
authenticate,
|
authenticate,
|
||||||
oidcAuthenticate,
|
oidcAuthenticate,
|
||||||
oidcProviderName,
|
|
||||||
oidcLoggingIn,
|
oidcLoggingIn,
|
||||||
passwordIcon,
|
passwordIcon,
|
||||||
inputType,
|
inputType,
|
||||||
|
|||||||
14
frontend/plugins/app-info.client.ts
Normal file
14
frontend/plugins/app-info.client.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import type { AppInfo } from "~/lib/api/types/admin";
|
||||||
|
|
||||||
|
export default defineNuxtPlugin({
|
||||||
|
async setup() {
|
||||||
|
const { data } = await axios.get<AppInfo>("/api/app/about");
|
||||||
|
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
appInfo: data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -128,11 +128,8 @@ class AppSettings(AppLoggingSettings):
|
|||||||
def validate_token_time(cls, v: int) -> int:
|
def validate_token_time(cls, v: int) -> int:
|
||||||
if v < 1:
|
if v < 1:
|
||||||
raise ValueError("TOKEN_TIME must be at least 1 hour")
|
raise ValueError("TOKEN_TIME must be at least 1 hour")
|
||||||
# If TOKEN_TIME is unreasonably high (e.g. hundreds of years), JWT encoding
|
# Certain browsers (webkit) have issues with very long-lived cookies, so we limit to 400 days
|
||||||
# can overflow, so we set the max to 10 years (87600 hours).
|
return min(v, 400 * 24)
|
||||||
if v > 87600:
|
|
||||||
raise ValueError("TOKEN_TIME is too high; maximum is 87600 hours (10 years)")
|
|
||||||
return v
|
|
||||||
|
|
||||||
SECRET: str
|
SECRET: str
|
||||||
SESSION_SECRET: str
|
SESSION_SECRET: str
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class AdminAboutController(BaseAdminController):
|
|||||||
default_household=settings.DEFAULT_HOUSEHOLD,
|
default_household=settings.DEFAULT_HOUSEHOLD,
|
||||||
allow_signup=settings.ALLOW_SIGNUP,
|
allow_signup=settings.ALLOW_SIGNUP,
|
||||||
allow_password_login=settings.ALLOW_PASSWORD_LOGIN,
|
allow_password_login=settings.ALLOW_PASSWORD_LOGIN,
|
||||||
|
token_time=settings.TOKEN_TIME,
|
||||||
build_id=settings.GIT_COMMIT_HASH,
|
build_id=settings.GIT_COMMIT_HASH,
|
||||||
recipe_scraper_version=recipe_scraper_version.__version__,
|
recipe_scraper_version=recipe_scraper_version.__version__,
|
||||||
enable_oidc=settings.OIDC_AUTH_ENABLED,
|
enable_oidc=settings.OIDC_AUTH_ENABLED,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ def get_app_info(session: Session = Depends(generate_session)):
|
|||||||
enable_openai=settings.OPENAI_ENABLED,
|
enable_openai=settings.OPENAI_ENABLED,
|
||||||
enable_openai_image_services=settings.OPENAI_ENABLED and settings.OPENAI_ENABLE_IMAGE_SERVICES,
|
enable_openai_image_services=settings.OPENAI_ENABLED and settings.OPENAI_ENABLE_IMAGE_SERVICES,
|
||||||
allow_password_login=settings.ALLOW_PASSWORD_LOGIN,
|
allow_password_login=settings.ALLOW_PASSWORD_LOGIN,
|
||||||
|
token_time=settings.TOKEN_TIME,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Annotated, Literal
|
from typing import Annotated
|
||||||
|
|
||||||
from authlib.integrations.starlette_client import OAuth
|
from authlib.integrations.starlette_client import OAuth
|
||||||
from fastapi import APIRouter, Depends, Header, Request, Response, status
|
from fastapi import APIRouter, Depends, Header, Request, Response, status
|
||||||
@@ -54,59 +54,13 @@ class MealieAuthToken(BaseModel):
|
|||||||
access_token: str
|
access_token: str
|
||||||
token_type: str = "bearer"
|
token_type: str = "bearer"
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def set_cookie(
|
|
||||||
cls, response: Response, token: str, *, expires_in: int | float | None = None, samesite: str | None = None
|
|
||||||
):
|
|
||||||
expires_in = int(expires_in) if expires_in else None
|
|
||||||
|
|
||||||
# httponly=False to allow JS access for frontend
|
|
||||||
response.set_cookie(
|
|
||||||
key="mealie.access_token",
|
|
||||||
value=token,
|
|
||||||
httponly=False,
|
|
||||||
max_age=expires_in,
|
|
||||||
secure=settings.PRODUCTION,
|
|
||||||
samesite=samesite,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def respond(cls, token: str, token_type: str = "bearer") -> dict:
|
def respond(cls, token: str, token_type: str = "bearer") -> dict:
|
||||||
return cls(access_token=token, token_type=token_type).model_dump()
|
return cls(access_token=token, token_type=token_type).model_dump()
|
||||||
|
|
||||||
|
|
||||||
def get_samesite(request: Request) -> Literal["lax", "none"]:
|
|
||||||
"""
|
|
||||||
Determine the appropriate samesite attribute for cookies.
|
|
||||||
|
|
||||||
`samesite="none"` is required for iframe support (i.e. embedding Mealie in another site)
|
|
||||||
but only works over HTTPS. If `samesite="none"` is set over HTTP, most browsers will reject the cookie.
|
|
||||||
|
|
||||||
`samesite="lax"` is the default, which works regardless of HTTP or HTTPS,
|
|
||||||
but does not support hosting in iframes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
forwarded_proto = request.headers.get("x-forwarded-proto", "").lower()
|
|
||||||
is_https = request.url.scheme == "https" or forwarded_proto == "https"
|
|
||||||
|
|
||||||
if is_https and settings.PRODUCTION:
|
|
||||||
return "none"
|
|
||||||
else:
|
|
||||||
# TODO: remove this once we resolve pending iframe issues
|
|
||||||
if settings.PRODUCTION:
|
|
||||||
logger.debug("Setting samesite to 'lax' because connection is not HTTPS")
|
|
||||||
logger.debug(f"{request.url.scheme=} | {forwarded_proto=}")
|
|
||||||
|
|
||||||
return "lax"
|
|
||||||
|
|
||||||
|
|
||||||
@public_router.post("/token")
|
@public_router.post("/token")
|
||||||
def get_token(
|
def get_token(request: Request, data: CredentialsRequestForm = Depends(), session: Session = Depends(generate_session)):
|
||||||
request: Request,
|
|
||||||
response: Response,
|
|
||||||
data: CredentialsRequestForm = Depends(),
|
|
||||||
session: Session = Depends(generate_session),
|
|
||||||
):
|
|
||||||
if "x-forwarded-for" in request.headers:
|
if "x-forwarded-for" in request.headers:
|
||||||
ip = request.headers["x-forwarded-for"]
|
ip = request.headers["x-forwarded-for"]
|
||||||
if "," in ip: # if there are multiple IPs, the first one is canonically the true client
|
if "," in ip: # if there are multiple IPs, the first one is canonically the true client
|
||||||
@@ -128,15 +82,7 @@ def get_token(
|
|||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
|
|
||||||
access_token, duration = auth
|
access_token, _ = auth
|
||||||
expires_in = duration.total_seconds() if duration else None
|
|
||||||
|
|
||||||
MealieAuthToken.set_cookie(
|
|
||||||
response,
|
|
||||||
access_token,
|
|
||||||
expires_in=expires_in,
|
|
||||||
samesite=get_samesite(request),
|
|
||||||
)
|
|
||||||
return MealieAuthToken.respond(access_token)
|
return MealieAuthToken.respond(access_token)
|
||||||
|
|
||||||
|
|
||||||
@@ -160,7 +106,7 @@ async def oauth_login(request: Request):
|
|||||||
|
|
||||||
|
|
||||||
@public_router.get("/oauth/callback")
|
@public_router.get("/oauth/callback")
|
||||||
async def oauth_callback(request: Request, response: Response, session: Session = Depends(generate_session)):
|
async def oauth_callback(request: Request, session: Session = Depends(generate_session)):
|
||||||
if not oauth:
|
if not oauth:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
@@ -186,15 +132,7 @@ async def oauth_callback(request: Request, response: Response, session: Session
|
|||||||
if not auth:
|
if not auth:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
access_token, duration = auth
|
access_token, _ = auth
|
||||||
expires_in = duration.total_seconds() if duration else None
|
|
||||||
|
|
||||||
MealieAuthToken.set_cookie(
|
|
||||||
response,
|
|
||||||
access_token,
|
|
||||||
expires_in=expires_in,
|
|
||||||
samesite=get_samesite(request),
|
|
||||||
)
|
|
||||||
return MealieAuthToken.respond(access_token)
|
return MealieAuthToken.respond(access_token)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class AppInfo(MealieModel):
|
|||||||
oidc_provider_name: str
|
oidc_provider_name: str
|
||||||
enable_openai: bool
|
enable_openai: bool
|
||||||
enable_openai_image_services: bool
|
enable_openai_image_services: bool
|
||||||
|
token_time: int
|
||||||
|
|
||||||
|
|
||||||
class AppTheme(MealieModel):
|
class AppTheme(MealieModel):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { defineConfig, devices } from '@playwright/test';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './.',
|
testDir: './.',
|
||||||
/* Run tests in files in parallel */
|
/* Run tests in files in parallel */
|
||||||
fullyParallel: true,
|
fullyParallel: false,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
|
|||||||
Reference in New Issue
Block a user