Ground-Zerro / Phobos Public
Code Issues Pull requests Actions Releases View on GitHub ↗
3.7 KB plaintext
<template>
  <BaseDialog v-model:open="open" :trigger-class="triggerClass">
    <template #trigger>
      <slot />
    </template>
    <template #title>
      {{ $t('client.new') }}
    </template>
    <template #description>
      <div class="flex flex-col gap-2">
        <div class="flex flex-col gap-1">
          <FormTextField
            id="name"
            v-model="name"
            :label="$t('client.name')"
            :description="$t('client.nameHint')"
          />
          <p v-if="nameError" class="text-xs text-red-500">
            {{ nameError }}
          </p>
        </div>
        <FormDateField
          id="expiresAt"
          v-model="expiresAt"
          :label="$t('client.expireDate')"
        />
        <div class="flex flex-col gap-1">
          <FormLabel for="presetId">{{ $t('client.preset') }}</FormLabel>
          <select
            id="presetId"
            v-model="presetId"
            class="rounded border-2 border-gray-100 px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-800"
          >
            <option :value="null">{{ $t('client.presetUseDefault') }}</option>
            <option v-for="p in selectablePresets" :key="p.id" :value="p.id">
              {{ p.name }} ({{ p.mode === 'SOCKS5' ? 'SOCKS5' : 'WireGuard' }})
            </option>
          </select>
        </div>
      </div>
    </template>
    <template #actions>
      <DialogClose as-child>
        <BaseSecondaryButton>{{ $t('dialog.cancel') }}</BaseSecondaryButton>
      </DialogClose>
      <BasePrimaryButton
        :disabled="!nameValid"
        :class="{ 'cursor-not-allowed opacity-50': !nameValid }"
        @click="createClient"
      >
        {{ $t('client.create') }}
      </BasePrimaryButton>
    </template>
  </BaseDialog>
</template>

<script lang="ts" setup>
type PresetSummary = {
  id: number;
  name: string;
  isDefault: boolean;
  mode: 'WIREGUARD' | 'SOCKS5';
};

const open = ref(false);
const name = ref<string>('');
const expiresAt = ref<string | null>(null);
const presetId = ref<number | null>(null);
const clientsStore = useClientsStore();

const { t } = useI18n();

defineProps<{ triggerClass?: string }>();

const { data: presets } = await useFetch<PresetSummary[]>(
  '/api/admin/obfuscator-presets',
  { method: 'get', default: () => [] }
);

const selectablePresets = computed(() =>
  (presets.value ?? []).filter((p) => !p.isDefault)
);

const existingNames = computed(
  () =>
    new Set(
      (clientsStore.clients ?? []).map((c) => normalizeClientName(c.name))
    )
);

const nameError = computed(() => {
  const value = name.value.trim();
  if (value.length === 0) {
    return null;
  }
  switch (validateClientName(value)) {
    case 'tooLong':
      return t('client.nameError.tooLong', { max: CLIENT_NAME_MAX_LENGTH });
    case 'invalidChars':
      return t('client.nameError.invalidChars');
  }
  if (existingNames.value.has(normalizeClientName(value))) {
    return t('client.nameError.duplicate');
  }
  return null;
});

const nameValid = computed(() => {
  const value = name.value.trim();
  return (
    value.length > 0 &&
    validateClientName(value) === null &&
    !existingNames.value.has(normalizeClientName(value))
  );
});

function createClient() {
  if (!nameValid.value) {
    return;
  }
  return _createClient({
    name: name.value.trim(),
    expiresAt: expiresAt.value,
    presetId: presetId.value,
  });
}

const _createClient = useSubmit(
  '/api/client',
  {
    method: 'post',
  },
  {
    revert: async (success) => {
      await clientsStore.refresh();
      if (success) {
        open.value = false;
        name.value = '';
        expiresAt.value = null;
        presetId.value = null;
      }
    },
    successMsg: t('client.created'),
  }
);
</script>