fix text inputs not validating right away

closes #1017
This commit is contained in:
Jake Potrebic 2022-11-20 19:27:42 -08:00
parent 31fe7bb53e
commit 7a77c88bb8
No known key found for this signature in database
GPG Key ID: 27CC63F7CBC866C7
14 changed files with 63 additions and 46 deletions

View File

@ -10,8 +10,9 @@ insert_final_newline = true
ij_any_block_comment_at_first_column = false
ij_any_line_comment_at_first_column = false
ij_any_block_comment_add_space = true
ij_typescript_use_double_quotes = false
ij_javascript_use_double_quotes = false
max_line_length = 160
ij_visual_guides = 160
ij_sass_use_double_quotes = false
ij_html_quote_style = double
@ -21,11 +22,11 @@ ij_java_use_fq_class_names = false
ij_java_class_count_to_use_import_on_demand = 99999
ij_java_names_count_to_use_import_on_demand = 99999
ij_java_imports_layout = *,|,$*
ij_java_generate_final_locals = true
ij_java_generate_final_parameters = true
ij_java_continuation_indent_size = 4
[*.yml]
indent_size = 2
[*.yaml]
[{*.yml, *.yaml}]
indent_size = 2
[*.md]

View File

@ -1,9 +1,11 @@
package io.papermc.hangar.config.hangar;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.model.internal.api.responses.Validation;
import io.papermc.hangar.util.PatternWrapper;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.http.HttpStatus;
@ConfigurationProperties(prefix = "hangar.orgs")
public record OrganizationsConfig(
@ -12,9 +14,16 @@ public record OrganizationsConfig(
@DefaultValue("5") int createLimit,
@DefaultValue("3") int minNameLen,
@DefaultValue("20") int maxNameLen,
@DefaultValue("[a-zA-Z0-9-_]*") PatternWrapper nameRegex
@DefaultValue("^[a-zA-Z0-9-_]+$") PatternWrapper nameRegex
) {
public boolean testOrgName(final String name) {
if (name.length() > this.maxNameLen() || name.length() < this.minNameLen() || !this.nameRegex().test(name)) {
throw new HangarApiException(HttpStatus.BAD_REQUEST, "organization.new.error.invalidName");
}
return true;
}
public Validation orgName() {
return new Validation(this.nameRegex(), this.maxNameLen(), this.minNameLen());
}

View File

@ -62,7 +62,7 @@ public class OrganizationController extends HangarComponent {
private final ValidationService validationService;
@Autowired
public OrganizationController(UserService userService, OrganizationFactory organizationFactory, OrganizationService organizationService, OrganizationMemberService memberService, OrganizationInviteService inviteService, AuthenticationService authenticationService, ValidationService validationService) {
public OrganizationController(final UserService userService, final OrganizationFactory organizationFactory, final OrganizationService organizationService, final OrganizationMemberService memberService, final OrganizationInviteService inviteService, final AuthenticationService authenticationService, final ValidationService validationService) {
this.userService = userService;
this.organizationFactory = organizationFactory;
this.organizationService = organizationService;
@ -75,11 +75,11 @@ public class OrganizationController extends HangarComponent {
@Anyone
@ResponseStatus(HttpStatus.OK)
@GetMapping("/validate")
public void validateName(@RequestParam String name) {
if (!validationService.isValidUsername(name)) {
public void validateName(@RequestParam final String name) {
if (!this.validationService.isValidOrgName(name)) {
throw new HangarApiException("author.error.invalidUsername");
}
if (userService.getUserTable(name) != null) {
if (this.userService.getUserTable(name) != null) {
throw new HangarApiException(HttpStatus.BAD_REQUEST);
}
}

View File

@ -1,47 +1,45 @@
package io.papermc.hangar.service;
import io.papermc.hangar.config.hangar.HangarConfig;
import io.papermc.hangar.exceptions.HangarApiException;
import io.papermc.hangar.service.internal.projects.ProjectFactory;
import io.papermc.hangar.util.StringUtils;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Service;
import java.util.Set;
import io.papermc.hangar.config.hangar.HangarConfig;
import io.papermc.hangar.util.StringUtils;
@Service
public class ValidationService {
private static final Set<String> BANNED_ROUTES = Set.of("api", "authors", "linkout", "logged-out", "new", "unread", "notifications", "staff", "admin", "organizations", "tools", "recommended", "null", "undefined", "tos", "settings");
private final HangarConfig config;
private static final Set<String> bannedRoutes = Set.of("api", "authors", "linkout", "logged-out", "new", "unread", "notifications", "staff", "admin", "organizations", "tools", "recommended", "null", "undefined", "tos", "settings");
public ValidationService(HangarConfig config) {
public ValidationService(final HangarConfig config) {
this.config = config;
}
public boolean isValidOrgName(final String name) {
return this.config.org.testOrgName(name) && this.isValidUsername(name);
}
public boolean isValidUsername(String name) {
name = StringUtils.compact(name);
if (bannedRoutes.contains(name)) {
if (BANNED_ROUTES.contains(name)) {
return false;
}
if (name.length() < 1) {
return false;
}
return true;
return name.length() >= 1;
}
public @Nullable String isValidProjectName(String name) {
name = StringUtils.compact(name);
String error = null;
if (bannedRoutes.contains(name)) {
if (BANNED_ROUTES.contains(name)) {
error = "invalidName";
} else if (name.length() < 3) {
error = "tooShortName";
} else if (name.length() > config.projects.maxNameLen()) {
} else if (name.length() > this.config.projects.maxNameLen()) {
error = "tooLongName";
} else if (name.contains(ProjectFactory.SOFT_DELETION_SUFFIX) || !config.projects.nameRegex().test(name)) {
} else if (name.contains(ProjectFactory.SOFT_DELETION_SUFFIX) || !this.config.projects.nameRegex().test(name)) {
error = "invalidName";
}
return error != null ? "project.new.error." + error : null;
@ -49,21 +47,18 @@ public class ValidationService {
public boolean isValidVersionName(String name) {
name = StringUtils.compact(name);
if (bannedRoutes.contains(name) || name.contains(ProjectFactory.SOFT_DELETION_SUFFIX)) {
if (BANNED_ROUTES.contains(name) || name.contains(ProjectFactory.SOFT_DELETION_SUFFIX)) {
return false;
}
if (name.length() < 1 || name.length() > config.projects.maxVersionNameLen() || !config.projects.versionNameRegex().test(name)) {
return false;
}
return true;
return name.length() >= 1 && name.length() <= this.config.projects.maxVersionNameLen() && this.config.projects.versionNameRegex().test(name);
}
public void testPageName(String name) {
name = StringUtils.compact(name);
if (bannedRoutes.contains(name)) {
if (BANNED_ROUTES.contains(name)) {
throw new HangarApiException("page.new.error.invalidName");
}
config.pages.testPageName(name);
this.config.pages.testPageName(name);
}
}

View File

@ -112,7 +112,7 @@ public class ProjectFactory extends HangarComponent {
}
public void checkProjectAvailability(final long userId, final String name) {
checkProjectAvailability(userId, name, false);
this.checkProjectAvailability(userId, name, false);
}
public void checkProjectAvailability(final long userId, final String name, final boolean skipNameChecking) {

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { useSettingsStore } from "~/store/settings";
import { useSettingsStore } from "~/store/useSettingsStore";
import { useHead } from "@vueuse/head";
import { settingsLog } from "~/lib/composables/useLog";
import { useAuthStore } from "~/store/auth";

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
import { useI18n } from "vue-i18n";
import { useSettingsStore } from "~/store/settings";
import { useSettingsStore } from "~/store/useSettingsStore";
import Announcement from "~/components/Announcement.vue";
import DropdownButton from "~/lib/components/design/DropdownButton.vue";
import DropdownItem from "~/lib/components/design/DropdownItem.vue";

@ -1 +1 @@
Subproject commit 03f449ccfbbf9c813f4675ec6505ddc818910944
Subproject commit 39d774394e68b982a56b7c28b06d50687be5c027

View File

@ -11,7 +11,7 @@ import { hasPerms, toNamedPermission } from "~/composables/usePerm";
import { NamedPermission, PermissionType } from "~/types/enums";
import { handleRequestError } from "~/composables/useErrorHandling";
import { useI18n } from "vue-i18n";
import { useSettingsStore } from "~/store/settings";
import { useSettingsStore } from "~/store/useSettingsStore";
import * as domain from "~/composables/useDomain";
export const install: UserModule = async ({ request, response, router, redirect }) => {

View File

@ -258,7 +258,7 @@ useHead(
<Tabs v-model="selectedTab" :tabs="tabs">
<template #general>
<ProjectSettingsSection title="project.settings.category" description="project.settings.categorySub">
<InputSelect v-model="form.category" :values="backendData.categoryOptions" :rules="[required()]" />
<InputSelect v-model="form.category" :values="backendData.categoryOptions" :rules="[required()]" i18n-text-values />
</ProjectSettingsSection>
<ProjectSettingsSection title="project.settings.description" description="project.settings.descriptionSub">
<InputText

View File

@ -11,7 +11,7 @@ import { useRoute, useRouter } from "vue-router";
import { useSeo } from "~/composables/useSeo";
import { useHead } from "@vueuse/head";
import Steps, { Step } from "~/lib/components/design/Steps.vue";
import { useSettingsStore } from "~/store/settings";
import { useSettingsStore } from "~/store/useSettingsStore";
import InputSelect from "~/lib/components/ui/InputSelect.vue";
import InputText from "~/lib/components/ui/InputText.vue";
import InputTag from "~/lib/components/ui/InputTag.vue";
@ -202,6 +202,7 @@ function createProject() {
:values="backendData.categoryOptions"
:label="i18n.t('project.new.step2.projectCategory')"
:rules="[required()]"
i18n-text-values
/>
</div>
</div>

View File

@ -7,7 +7,6 @@ import { NamedPermission, Platform, ProjectCategory, Prompt } from "~/types/enum
import { Announcement as AnnouncementObject, Announcement, IPermission, Role } from "hangar-api";
import { fetchIfNeeded, useInternalApi } from "~/composables/useApi";
import { Option } from "~/lib/components/ui/InputSelect.vue";
import { useI18n } from "vue-i18n";
interface Validation {
regex?: string;
@ -109,13 +108,11 @@ export const useBackendDataStore = defineStore("backendData", () => {
}
}
const visibleCategories = computed(() => [...(projectCategories.value?.values() || [])].filter((value) => value.visible));
const visibleCategories = computed<IProjectCategory[]>(() => [...(projectCategories.value?.values() || [])].filter((value) => value.visible));
const visiblePlatforms = computed(() => (platforms.value ? [...platforms.value.values()].filter((value) => value.visible) : []));
const licenseOptions = computed<Option[]>(() => licenses.value.map<Option>((l) => ({ value: l, text: l })));
const categoryOptions = computed<Option[]>(() =>
visibleCategories.value.map<Option>((c) => ({ value: c.apiName, text: useI18n({ useScope: "global" }).t(c.title) }))
);
const categoryOptions = computed<Option[]>(() => visibleCategories.value.map<Option>((c) => ({ value: c.apiName, text: c.title })));
return {
projectCategories,

View File

@ -7,6 +7,7 @@ export {};
declare module "@vue/runtime-core" {
export interface GlobalComponents {
IconMdiAccountPlus: typeof import("~icons/mdi/account-plus")["default"];
IconMdiAlert: typeof import("~icons/mdi/alert")["default"];
IconMdiAlertBox: typeof import("~icons/mdi/alert-box")["default"];
IconMdiAlertOutline: typeof import("~icons/mdi/alert-outline")["default"];
@ -16,11 +17,17 @@ declare module "@vue/runtime-core" {
IconMdiChat: typeof import("~icons/mdi/chat")["default"];
IconMdiCheck: typeof import("~icons/mdi/check")["default"];
IconMdiCheckBold: typeof import("~icons/mdi/check-bold")["default"];
IconMdiChevronDoubleDown: typeof import("~icons/mdi/chevron-double-down")["default"];
IconMdiCircle: typeof import("~icons/mdi/circle")["default"];
IconMdiClipboardOutline: typeof import("~icons/mdi/clipboard-outline")["default"];
IconMdiClose: typeof import("~icons/mdi/close")["default"];
IconMdiCloseCircle: typeof import("~icons/mdi/close-circle")["default"];
IconMdiCloudSearch: typeof import("~icons/mdi/cloud-search")["default"];
IconMdiCodeBracesBox: typeof import("~icons/mdi/code-braces-box")["default"];
IconMdiCogTransfer: typeof import("~icons/mdi/cog-transfer")["default"];
IconMdiContentSave: typeof import("~icons/mdi/content-save")["default"];
IconMdiController: typeof import("~icons/mdi/controller")["default"];
IconMdiDeleteAlert: typeof import("~icons/mdi/delete-alert")["default"];
IconMdiDownload: typeof import("~icons/mdi/download")["default"];
IconMdiEarth: typeof import("~icons/mdi/earth")["default"];
IconMdiEye: typeof import("~icons/mdi/eye")["default"];
@ -32,12 +39,19 @@ declare module "@vue/runtime-core" {
IconMdiHorseVariant: typeof import("~icons/mdi/horse-variant")["default"];
IconMdiInformation: typeof import("~icons/mdi/information")["default"];
IconMdiKeyOutline: typeof import("~icons/mdi/key-outline")["default"];
IconMdiLicense: typeof import("~icons/mdi/license")["default"];
IconMdiLink: typeof import("~icons/mdi/link")["default"];
IconMdiLockOpenOutline: typeof import("~icons/mdi/lock-open-outline")["default"];
IconMdiLockOutline: typeof import("~icons/mdi/lock-outline")["default"];
IconMdiMenu: typeof import("~icons/mdi/menu")["default"];
IconMdiOpenInNew: typeof import("~icons/mdi/open-in-new")["default"];
IconMdiPencil: typeof import("~icons/mdi/pencil")["default"];
IconMdiRenameBox: typeof import("~icons/mdi/rename-box")["default"];
IconMdiShape: typeof import("~icons/mdi/shape")["default"];
IconMdiShieldSun: typeof import("~icons/mdi/shield-sun")["default"];
IconMdiSortVariant: typeof import("~icons/mdi/sort-variant")["default"];
IconMdiStar: typeof import("~icons/mdi/star")["default"];
IconMdiSubdirectoryArrowLeft: typeof import("~icons/mdi/subdirectory-arrow-left")["default"];
IconMdiTools: typeof import("~icons/mdi/tools")["default"];
IconMdiTrophy: typeof import("~icons/mdi/trophy")["default"];
IconMdiWeatherNight: typeof import("~icons/mdi/weather-night")["default"];