Fix/fix block registration (#1059)

* fix disable button

* add backend env for restricting registration

* update state management

* add allow_signup to app info

* move allow_signup to backend only

* cleanup docker-compose

* potential darkmode fix

* fix missing variable

* add banner on login page

* use random bools for tests

* fix initial state bug

* fix state reset
This commit is contained in:
Hayden 2022-03-15 17:34:53 -08:00 committed by GitHub
parent 3c2744a3da
commit 13e157827c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 107 additions and 52 deletions

View File

@ -12,7 +12,6 @@ services:
ports: ports:
- 9091:3000 - 9091:3000
environment: environment:
- ALLOW_SIGNUP=true
- API_URL=http://mealie-api:9000 - API_URL=http://mealie-api:9000
# ===================================== # =====================================
@ -45,6 +44,8 @@ services:
ports: ports:
- 9092:9000 - 9092:9000
environment: environment:
ALLOW_SIGNUP: "false"
DB_ENGINE: sqlite # Optional: 'sqlite', 'postgres' DB_ENGINE: sqlite # Optional: 'sqlite', 'postgres'
# ===================================== # =====================================
# Postgres Config # Postgres Config

View File

@ -15,6 +15,7 @@
| 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 | true | Allow user sign-up without token (should match frontend env) |

View File

@ -4,10 +4,9 @@
### General ### General
| Variables | Default | Description | | Variables | Default | Description |
| ------------ | :--------------------: | ----------------------------------- | | --------- | :--------------------: | ------------------------- |
| ALLOW_SIGNUP | true | Allows anyone to sign-up for Mealie | | API_URL | http://mealie-api:9000 | URL to proxy API requests |
| API_URL | http://mealie-api:9000 | URL to proxy API requests |
### Themeing ### Themeing
Setting the following environmental variables will change the theme of the frontend. Note that the themes are the same for all users. This is a break-change when migration from v0.x.x -> 1.x.x. Setting the following environmental variables will change the theme of the frontend. Note that the themes are the same for all users. This is a break-change when migration from v0.x.x -> 1.x.x.

View File

@ -18,7 +18,6 @@ services:
- mealie-api - mealie-api
environment: environment:
# Set Frontend ENV Variables Here # Set Frontend ENV Variables Here
- ALLOW_SIGNUP=true
- API_URL=http://mealie-api:9000 # (1) - API_URL=http://mealie-api:9000 # (1)
restart: always restart: always
ports: ports:
@ -34,6 +33,7 @@ services:
- mealie-data:/app/data/ - mealie-data:/app/data/
environment: environment:
# Set Backend ENV Variables Here # Set Backend ENV Variables Here
- ALLOW_SIGNUP=true
- PUID=1000 - PUID=1000
- PGID=1000 - PGID=1000
- TZ=America/Anchorage - TZ=America/Anchorage

View File

@ -16,7 +16,6 @@ services:
container_name: mealie-frontend container_name: mealie-frontend
environment: environment:
# Set Frontend ENV Variables Here # Set Frontend ENV Variables Here
- ALLOW_SIGNUP=true
- API_URL=http://mealie-api:9000 # (1) - API_URL=http://mealie-api:9000 # (1)
restart: always restart: always
ports: ports:
@ -30,6 +29,7 @@ services:
- mealie-data:/app/data/ - mealie-data:/app/data/
environment: environment:
# Set Backend ENV Variables Here # Set Backend ENV Variables Here
- ALLOW_SIGNUP=true
- PUID=1000 - PUID=1000
- PGID=1000 - PGID=1000
- TZ=America/Anchorage - TZ=America/Anchorage

View File

@ -1,2 +1,3 @@
export { useAppInfo } from "./use-app-info";
export { useStaticRoutes } from "./static-routes"; export { useStaticRoutes } from "./static-routes";
export { useAdminApi, useUserApi } from "./api-client"; export { useAdminApi, useUserApi } from "./api-client";

View File

@ -0,0 +1,11 @@
import { Ref, useAsync } from "@nuxtjs/composition-api";
import { useAsyncKey } from "../use-utils";
import { AppInfo } from "~/types/api-types/admin";
export function useAppInfo(): Ref<AppInfo | null> {
return useAsync(async () => {
// We use fetch here to reduce need for additional dependencies
const data = await fetch("/api/app/about").then((res) => res.json());
return data as AppInfo;
}, useAsyncKey());
}

View File

@ -2,6 +2,12 @@
<v-app dark> <v-app dark>
<TheSnackbar /> <TheSnackbar />
<v-banner v-if="isDemo" sticky>
<div class="text-center">
<b> This is a Demo for version: {{ version }} </b> | Username: changeme@email.com | Password: demo
</div>
</v-banner>
<v-main> <v-main>
<v-scroll-x-transition> <v-scroll-x-transition>
<Nuxt /> <Nuxt />
@ -11,9 +17,23 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api"; import { computed, defineComponent } from "@nuxtjs/composition-api";
import TheSnackbar from "~/components/Layout/TheSnackbar.vue"; import TheSnackbar from "~/components/Layout/TheSnackbar.vue";
import { useAppInfo } from "~/composables/api";
export default defineComponent({ export default defineComponent({
components: { TheSnackbar }, components: { TheSnackbar },
setup() {
const appInfo = useAppInfo();
const isDemo = computed(() => appInfo?.value?.demoStatus || false);
const version = computed(() => appInfo?.value?.version || "unknown");
return {
appInfo,
isDemo,
version,
};
},
}); });
</script> </script>

View File

@ -26,7 +26,6 @@ export default {
env: { env: {
GLOBAL_MIDDLEWARE: process.env.GLOBAL_MIDDLEWARE || null, GLOBAL_MIDDLEWARE: process.env.GLOBAL_MIDDLEWARE || null,
ALLOW_SIGNUP: process.env.ALLOW_SIGNUP || true,
}, },
router: { router: {
@ -220,10 +219,6 @@ export default {
publicRuntimeConfig: { publicRuntimeConfig: {
GLOBAL_MIDDLEWARE: process.env.GLOBAL_MIDDLEWARE || null, GLOBAL_MIDDLEWARE: process.env.GLOBAL_MIDDLEWARE || null,
ALLOW_SIGNUP: process.env.ALLOW_SIGNUP || true,
envProps: {
allowSignup: process.env.ALLOW_SIGNUP || true,
},
SUB_PATH: process.env.SUB_PATH || "", SUB_PATH: process.env.SUB_PATH || "",
axios: { axios: {
browserBaseURL: process.env.SUB_PATH || "", browserBaseURL: process.env.SUB_PATH || "",

View File

@ -4,7 +4,7 @@
fluid fluid
class="d-flex justify-center align-center" class="d-flex justify-center align-center"
:class="{ :class="{
'bg-off-white': !$vuetify.theme.dark, 'bg-off-white': !$vuetify.theme.dark && !isDark,
}" }"
> >
<v-card tag="section" class="d-flex flex-column align-center" width="600px"> <v-card tag="section" class="d-flex flex-column align-center" width="600px">
@ -108,6 +108,8 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, useContext, computed, reactive } from "@nuxtjs/composition-api"; import { defineComponent, ref, useContext, computed, reactive } from "@nuxtjs/composition-api";
import { useDark } from "@vueuse/core";
import { useAppInfo } from "~/composables/api";
import { alert } from "~/composables/use-toast"; import { alert } from "~/composables/use-toast";
import { useToggleDarkMode } from "~/composables/use-utils"; import { useToggleDarkMode } from "~/composables/use-utils";
export default defineComponent({ export default defineComponent({
@ -115,9 +117,9 @@ export default defineComponent({
setup() { setup() {
const toggleDark = useToggleDarkMode(); const toggleDark = useToggleDarkMode();
const isDark = useDark();
const { $auth } = useContext(); const { $auth } = useContext();
const context = useContext();
const form = reactive({ const form = reactive({
email: "", email: "",
@ -127,7 +129,9 @@ export default defineComponent({
const loggingIn = ref(false); const loggingIn = ref(false);
const allowSignup = computed(() => context.env.ALLOW_SIGNUP as boolean); const appInfo = useAppInfo();
const allowSignup = computed(() => appInfo.value?.allowSignup || false);
async function authenticate() { async function authenticate() {
if (form.email.length === 0 || form.password.length === 0) { if (form.email.length === 0 || form.password.length === 0) {
@ -148,6 +152,7 @@ export default defineComponent({
// See https://github.com/nuxt-community/axios-module/issues/550 // See https://github.com/nuxt-community/axios-module/issues/550
// Import $axios from useContext() // Import $axios from useContext()
// if ($axios.isAxiosError(error) && error.response?.status === 401) { // if ($axios.isAxiosError(error) && error.response?.status === 401) {
// @ts-ignore - see above
if (error.response?.status === 401) { if (error.response?.status === 401) {
alert.error("Invalid Credentials"); alert.error("Invalid Credentials");
} else { } else {
@ -158,6 +163,7 @@ export default defineComponent({
} }
return { return {
isDark,
form, form,
loggingIn, loggingIn,
allowSignup, allowSignup,

View File

@ -6,8 +6,8 @@
<v-form ref="domRegisterForm" @submit.prevent="register()"> <v-form ref="domRegisterForm" @submit.prevent="register()">
<div class="d-flex justify-center my-2"> <div class="d-flex justify-center my-2">
<v-btn-toggle v-model="joinGroup" mandatory tile group color="primary"> <v-btn-toggle v-model="joinGroup" mandatory tile group color="primary">
<v-btn :value="false" small @click="joinGroup = false"> Create a Group </v-btn> <v-btn :value="false" small @click="toggleJoinGroup"> Create a Group </v-btn>
<v-btn :value="true" small @click="joinGroup = true"> Join a Group </v-btn> <v-btn :value="true" small @click="toggleJoinGroup"> Join a Group </v-btn>
</v-btn-toggle> </v-btn-toggle>
</div> </div>
<v-text-field <v-text-field
@ -99,12 +99,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, reactive, toRefs, ref, useRouter, watch } from "@nuxtjs/composition-api"; import { computed, defineComponent, reactive, toRefs, ref, useRouter } from "@nuxtjs/composition-api";
import { validators } from "@/composables/use-validators"; import { validators } from "@/composables/use-validators";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast"; import { alert } from "~/composables/use-toast";
import { useRouterQuery } from "@/composables/use-router"; import { useRouteQuery } from "@/composables/use-router";
import { VForm} from "~/types/vuetify"; import { VForm } from "~/types/vuetify";
export default defineComponent({ export default defineComponent({
layout: "basic", layout: "basic",
@ -117,18 +117,22 @@ export default defineComponent({
}); });
const allowSignup = computed(() => process.env.AllOW_SIGNUP); const allowSignup = computed(() => process.env.AllOW_SIGNUP);
const token = useRouterQuery("token"); const token = useRouteQuery("token");
watch(token, (newToken) => { if (token.value) {
if (newToken) {
form.groupToken = newToken;
}
});
if (token) {
state.joinGroup = true; state.joinGroup = true;
} }
function toggleJoinGroup() {
if (state.joinGroup) {
state.joinGroup = false;
token.value = "";
} else {
state.joinGroup = true;
form.group = "";
}
}
const domRegisterForm = ref<VForm | null>(null); const domRegisterForm = ref<VForm | null>(null);
const form = reactive({ const form = reactive({
@ -163,6 +167,7 @@ export default defineComponent({
return { return {
token, token,
toggleJoinGroup,
domRegisterForm, domRegisterForm,
validators, validators,
allowSignup, allowSignup,

View File

@ -9,6 +9,7 @@ export interface AdminAboutInfo {
production: boolean; production: boolean;
version: string; version: string;
demoStatus: boolean; demoStatus: boolean;
allowSignup: boolean;
versionLatest: string; versionLatest: string;
apiPort: number; apiPort: number;
apiDocs: boolean; apiDocs: boolean;
@ -29,6 +30,7 @@ export interface AppInfo {
production: boolean; production: boolean;
version: string; version: string;
demoStatus: boolean; demoStatus: boolean;
allowSignup: boolean;
} }
export interface AppStatistics { export interface AppStatistics {
totalRecipes: number; totalRecipes: number;

View File

@ -11,9 +11,12 @@ export interface ErrorResponse {
exception?: string; exception?: string;
} }
export interface FileTokenResponse { export interface FileTokenResponse {
file_token: string; fileToken: string;
} }
export interface SuccessResponse { export interface SuccessResponse {
message: string; message: string;
error?: boolean; error?: boolean;
} }
export interface ValidationResponse {
valid?: boolean;
}

View File

@ -32,6 +32,8 @@ class AppSettings(BaseSettings):
TOKEN_TIME: int = 48 # Time in Hours TOKEN_TIME: int = 48 # Time in Hours
SECRET: str SECRET: str
ALLOW_SIGNUP: bool = True
@property @property
def DOCS_URL(self) -> str | None: def DOCS_URL(self) -> str | None:
return "/docs" if self.API_DOCS else None return "/docs" if self.API_DOCS else None
@ -119,8 +121,8 @@ def app_settings_constructor(data_dir: Path, production: bool, env_file: Path, e
directly, but rather through this factory function. directly, but rather through this factory function.
""" """
app_settings = AppSettings( app_settings = AppSettings(
_env_file=env_file, _env_file=env_file, # type: ignore
_env_file_encoding=env_encoding, _env_file_encoding=env_encoding, # type: ignore
**{"SECRET": determine_secrets(data_dir, production)}, **{"SECRET": determine_secrets(data_dir, production)},
) )

View File

@ -25,6 +25,7 @@ class AdminAboutController(BaseAdminController):
db_type=settings.DB_ENGINE, db_type=settings.DB_ENGINE,
db_url=settings.DB_URL_PUBLIC, db_url=settings.DB_URL_PUBLIC,
default_group=settings.DEFAULT_GROUP, default_group=settings.DEFAULT_GROUP,
allow_signup=settings.ALLOW_SIGNUP,
) )
@router.get("/statistics", response_model=AppStatistics) @router.get("/statistics", response_model=AppStatistics)

View File

@ -15,4 +15,5 @@ async def get_app_info():
version=APP_VERSION, version=APP_VERSION,
demo_status=settings.IS_DEMO, demo_status=settings.IS_DEMO,
production=settings.PRODUCTION, production=settings.PRODUCTION,
allow_signup=settings.ALLOW_SIGNUP,
) )

View File

@ -1,7 +1,9 @@
from fastapi import APIRouter, status from fastapi import APIRouter, HTTPException, status
from mealie.core.config import get_app_settings
from mealie.repos.all_repositories import get_repositories from mealie.repos.all_repositories import get_repositories
from mealie.routes._base import BasePublicController, controller from mealie.routes._base import BasePublicController, controller
from mealie.schema.response import ErrorResponse
from mealie.schema.user.registration import CreateUserRegistration from mealie.schema.user.registration import CreateUserRegistration
from mealie.schema.user.user import UserOut from mealie.schema.user.user import UserOut
from mealie.services.user_services.registration_service import RegistrationService from mealie.services.user_services.registration_service import RegistrationService
@ -13,5 +15,12 @@ router = APIRouter(prefix="/register")
class RegistrationController(BasePublicController): class RegistrationController(BasePublicController):
@router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED) @router.post("", response_model=UserOut, status_code=status.HTTP_201_CREATED)
def register_new_user(self, data: CreateUserRegistration): def register_new_user(self, data: CreateUserRegistration):
settings = get_app_settings()
if not settings.ALLOW_SIGNUP and data.group_token is None or data.group_token == "":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ErrorResponse.respond("User Registration is Disabled")
)
registration_service = RegistrationService(self.deps.logger, get_repositories(self.deps.session)) registration_service = RegistrationService(self.deps.logger, get_repositories(self.deps.session))
return registration_service.register_user(data) return registration_service.register_user(data)

View File

@ -1,5 +1,3 @@
from pathlib import Path
from fastapi_camelcase import CamelModel from fastapi_camelcase import CamelModel
@ -15,6 +13,7 @@ class AppInfo(CamelModel):
production: bool production: bool
version: str version: str
demo_status: bool demo_status: bool
allow_signup: bool
class AdminAboutInfo(AppInfo): class AdminAboutInfo(AppInfo):
@ -22,7 +21,7 @@ class AdminAboutInfo(AppInfo):
api_port: int api_port: int
api_docs: bool api_docs: bool
db_type: str db_type: str
db_url: Path db_url: str | None
default_group: str default_group: str

View File

@ -16,9 +16,8 @@ def test_get_preferences(api_client: TestClient, unique_user: TestUser) -> None:
preferences = response.json() preferences = response.json()
# Spot Check Defaults assert preferences["recipePublic"] in {True, False}
assert preferences["recipePublic"] is True assert preferences["recipeShowNutrition"] in {True, False}
assert preferences["recipeShowNutrition"] is False
def test_preferences_in_group(api_client: TestClient, unique_user: TestUser) -> None: def test_preferences_in_group(api_client: TestClient, unique_user: TestUser) -> None:
@ -31,8 +30,8 @@ def test_preferences_in_group(api_client: TestClient, unique_user: TestUser) ->
assert group["preferences"] is not None assert group["preferences"] is not None
# Spot Check # Spot Check
assert group["preferences"]["recipePublic"] is True assert group["preferences"]["recipePublic"] in {True, False}
assert group["preferences"]["recipeShowNutrition"] is False assert group["preferences"]["recipeShowNutrition"] in {True, False}
def test_update_preferences(api_client: TestClient, unique_user: TestUser) -> None: def test_update_preferences(api_client: TestClient, unique_user: TestUser) -> None:

View File

@ -16,13 +16,13 @@ def random_bool() -> bool:
return bool(random.getrandbits(1)) return bool(random.getrandbits(1))
def user_registration_factory() -> CreateUserRegistration: def user_registration_factory(advanced=None, private=None) -> CreateUserRegistration:
return CreateUserRegistration( return CreateUserRegistration(
group=random_string(), group=random_string(),
email=random_email(), email=random_email(),
username=random_string(), username=random_string(),
password="fake-password", password="fake-password",
password_confirm="fake-password", password_confirm="fake-password",
advanced=False, advanced=advanced or random_bool(),
private=False, private=private or random_bool(),
) )