Chg: exactOptionalPropertyTypesをfalseに変更 / Chg(Security): トークンが不正な場合のエラーを全てtoken_invalidに変更 / New: channelテーブル・リポジトリ / Chg: configテーブルのvalueをstringに / Chg: configテーブルのlengthを4096に / New: messageテーブル / Chg: 安全のためuserテーブルのOptionalPropsにidを追加 / New: channel/createエンドポイント / New: channel/listエンドポイント / New: channel/editエンドポイント / Enhance: primary/signupエンドポイントの重複エラーの実装で末尾カンマなどの改善 / Chg: setup/initializationのdescriptionに最大文字数4096を制定 / Chg: serverInfoをdefault exportからexportに変更 / New: フロントエンドでmeを読み込み / New: フロントエンドでchannelを読み込み / New: client.tsでトークンがある場合はトークンを指定 / Chg: clientをrefに / Del: IndexedDBからserverテーブルを削除 / Fix: Dexieのclassに命名 / Feat: フロントエンドでのサインインページ / Fix: L.jsで任意のbodyがあるエンドポイントが定義できない問題を修正 / Del: L.jsのserver-infoでの不要なimportを削除 / Fix: L.jsでトークンのエラーを追加 / Fix: L.jsのUserSchemaにlastUsedAtを追加
This commit is contained in:
@@ -125,7 +125,7 @@ main.layout {
|
||||
import { RouterView, useRouter } from "vue-router";
|
||||
import routerStatus from "@/lib/router";
|
||||
import Progress from "@/components/Progress.vue";
|
||||
import serverInfo from "@/lib/account";
|
||||
import { serverInfo } from "@/lib/account";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
@@ -1,11 +1,57 @@
|
||||
import client from "@/lib/client";
|
||||
import client, { initClient } from "@/lib/client";
|
||||
import type ApiMap from "lynqchat-js/1.0.0-alpha.0/map";
|
||||
import { ref } from "vue";
|
||||
/*
|
||||
[TODO]
|
||||
キャッシュの類を全部account.tsに詰め込むのをやめる
|
||||
*/
|
||||
|
||||
let serverInfo = ref<ApiMap["server-info"]["response"]>(await client.request("server-info"));
|
||||
await initClient();
|
||||
|
||||
export let serverInfo = ref<ApiMap["server-info"]["response"]>(await client.value.request("server-info"));
|
||||
export let account = ref<ApiMap["me"]["response"]>(await client.value.request("me"));
|
||||
|
||||
export let channels = ref<Extract<ApiMap["channel/list"]["response"], { channels: any }>["channels"]>([]);
|
||||
export let lastLoadedChannel = ref<string>();
|
||||
|
||||
export const reloadServerInfo = async () => {
|
||||
serverInfo.value = await client.request("server-info");
|
||||
serverInfo.value = await client.value.request("server-info");
|
||||
}
|
||||
|
||||
export default serverInfo;
|
||||
export const reloadAccount = async () => {
|
||||
await initClient();
|
||||
|
||||
account.value = await client.value.request("me");
|
||||
}
|
||||
|
||||
export const initChannels = async () => {
|
||||
lastLoadedChannel.value = undefined;
|
||||
|
||||
const response = await client.value.request("channel/list");
|
||||
|
||||
if (!response.success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
channels.value = response.channels;
|
||||
if (response.channels.length > 0) {
|
||||
lastLoadedChannel.value = response.channels[response.channels.length - 1]?.id;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export const loadChannels = async () => {
|
||||
const response = await client.value.request("channel/list", lastLoadedChannel.value ? {
|
||||
since: lastLoadedChannel.value,
|
||||
} : undefined);
|
||||
|
||||
if (!response.success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
channels.value = channels.value.concat(response.channels);
|
||||
if (response.channels.length > 0) {
|
||||
lastLoadedChannel.value = response.channels[response.channels.length - 1]?.id;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,8 +1,19 @@
|
||||
import LynqChat from "lynqchat-js";
|
||||
import type ApiMap from "lynqchat-js/1.0.0-alpha.0/map";
|
||||
import Database, { getByIndex } from "./db";
|
||||
import { ref } from "vue";
|
||||
|
||||
const client = new LynqChat<ApiMap>({
|
||||
const client = ref<LynqChat<ApiMap>>(new LynqChat<ApiMap>({
|
||||
origin: window.origin,
|
||||
});
|
||||
}));
|
||||
|
||||
export const initClient = async () => {
|
||||
const db = new Database();
|
||||
const row = await getByIndex(db.settings, "name", "token");
|
||||
|
||||
if (row?.value) {
|
||||
client.value.token = row.value;
|
||||
}
|
||||
}
|
||||
|
||||
export default client;
|
||||
@@ -6,14 +6,7 @@ export interface Settings {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Server {
|
||||
id: number;
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default class extends Dexie {
|
||||
server!: EntityTable<Server, "id">;
|
||||
export default class Database extends Dexie {
|
||||
settings!: EntityTable<Settings, "id">;
|
||||
|
||||
constructor() {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createApp } from "vue";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import routerStatus from "@/lib/router";
|
||||
import { createHead } from "@unhead/vue/client";
|
||||
import serverInfo from "@/lib/account";
|
||||
import { serverInfo } from "@/lib/account";
|
||||
|
||||
import "@/global.css";
|
||||
import Layout from "@/Layout.vue";
|
||||
@@ -39,6 +39,13 @@ const router = createRouter({
|
||||
},
|
||||
component: () => import("@/routes/setup/create-admin.vue"),
|
||||
},
|
||||
{
|
||||
path: "/signin",
|
||||
meta: {
|
||||
title: "サインイン",
|
||||
},
|
||||
component: () => import("@/routes/signin.vue"),
|
||||
},
|
||||
{
|
||||
path: "/:NotFound(.*)*",
|
||||
meta: {
|
||||
|
||||
@@ -3,4 +3,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { account } from "@/lib/account";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
if (!account.value.success) {
|
||||
router.replace("/signin");
|
||||
}
|
||||
</script>
|
||||
@@ -89,7 +89,7 @@ form {
|
||||
</style>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import serverInfo, { reloadServerInfo } from "@/lib/account";
|
||||
import { serverInfo, reloadServerInfo } from "@/lib/account";
|
||||
import { useRouter } from "vue-router";
|
||||
import Input from "@/components/Input.vue";
|
||||
import Button from "@/components/Button.vue";
|
||||
@@ -169,7 +169,7 @@ const submit = async (e: Event) => {
|
||||
});
|
||||
}
|
||||
|
||||
const response = await client.request("setup/create-admin", result.value.data);
|
||||
const response = await client.value.request("setup/create-admin", result.value.data);
|
||||
|
||||
if (!response.success) {
|
||||
closeLoadingModal();
|
||||
@@ -191,7 +191,7 @@ const submit = async (e: Event) => {
|
||||
onClose: async () => {
|
||||
isProcessing.value = false;
|
||||
await reloadServerInfo();
|
||||
router.push("/");
|
||||
router.push("/signin");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ form {
|
||||
</style>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import serverInfo, { reloadServerInfo } from "@/lib/account";
|
||||
import { serverInfo, reloadServerInfo } from "@/lib/account";
|
||||
import { useRouter } from "vue-router";
|
||||
import Input from "@/components/Input.vue";
|
||||
import Toggle from "@/components/Toggle.vue";
|
||||
@@ -163,7 +163,7 @@ const submit = async (e: Event) => {
|
||||
});
|
||||
}
|
||||
|
||||
const response = await client.request("setup/initialization", result.value.data);
|
||||
const response = await client.value.request("setup/initialization", result.value.data);
|
||||
|
||||
if (!response.success) {
|
||||
closeLoadingModal();
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<form novalidate @submit="submit">
|
||||
<Input
|
||||
label="ユーザーID"
|
||||
autocomplete="off"
|
||||
v-model="userid"
|
||||
>
|
||||
<span
|
||||
class="input-issue"
|
||||
v-if="useridIssue"
|
||||
>
|
||||
<Icon icon="material-symbols:error-outline-rounded" />
|
||||
{{ useridIssue.message }}
|
||||
</span>
|
||||
</Input>
|
||||
|
||||
<InputPassword
|
||||
label="パスワード"
|
||||
v-model="password"
|
||||
>
|
||||
<span
|
||||
class="input-issue"
|
||||
v-if="passwordIssue"
|
||||
>
|
||||
<Icon icon="material-symbols:error-outline-rounded" />
|
||||
{{ passwordIssue.message }}
|
||||
</span>
|
||||
</InputPassword>
|
||||
|
||||
<Button
|
||||
name="サインイン"
|
||||
type="submit"
|
||||
color="accent"
|
||||
:disabled="!result.success || isProcessing"
|
||||
/>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.input-issue {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--error-color);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.input-issue svg {
|
||||
font-size: 1rem;
|
||||
margin: auto 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Button from "@/components/Button.vue";
|
||||
import Input from "@/components/Input.vue";
|
||||
import InputPassword from "@/components/InputPassword.vue";
|
||||
import Loading from "@/components/Modal/Loading.vue";
|
||||
import Success from "@/components/Modal/Success.vue";
|
||||
import { account, reloadAccount, serverInfo } from "@/lib/account";
|
||||
import client from "@/lib/client";
|
||||
import Database from "@/lib/db";
|
||||
import { createModal } from "@/lib/modal";
|
||||
import { getIssueFromPath } from "@/lib/validation";
|
||||
import { Icon } from "@iconify/vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import z from "zod/v3";
|
||||
|
||||
const router = useRouter();
|
||||
const isProcessing = ref<boolean>(false);
|
||||
|
||||
if (account.value.success) {
|
||||
router.replace("/");
|
||||
}
|
||||
|
||||
if (!serverInfo.value.success) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (!serverInfo.value.isInitialized) {
|
||||
router.replace("/setup/initialization");
|
||||
}
|
||||
|
||||
const userid = ref<string>("");
|
||||
const password = ref<string>("");
|
||||
const useridIssue = computed(() => getIssueFromPath("userid", result.value));
|
||||
const passwordIssue = computed(() => getIssueFromPath("password", result.value));
|
||||
|
||||
const schema = z.object({
|
||||
userid: z.string({ message: "文字列で入力してください。" })
|
||||
.trim().min(3, "3文字以上で入力してください。")
|
||||
.max(20, "20文字以内で入力してください。"),
|
||||
password: z.string({ message: "文字列で入力してください。" })
|
||||
.trim().min(8, "8文字以上で入力してください。"),
|
||||
});
|
||||
|
||||
const result = computed(() => schema.safeParse({
|
||||
userid: userid.value,
|
||||
password: password.value,
|
||||
}));
|
||||
|
||||
const submit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
isProcessing.value = true;
|
||||
|
||||
const closeLoadingModal = createModal({
|
||||
component: Loading,
|
||||
});
|
||||
|
||||
if (!result.value.success) {
|
||||
const messages = result.value.error.issues.map(issue => issue.message);
|
||||
|
||||
closeLoadingModal();
|
||||
|
||||
return createModal({
|
||||
component: Error,
|
||||
onClose: () => isProcessing.value = false,
|
||||
props: {
|
||||
error: `不正な入力です。<br>${messages.join("<br>")}`,
|
||||
canClose: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const response = await client.value.request("primary/signin", result.value.data);
|
||||
|
||||
if (!response.success) {
|
||||
closeLoadingModal();
|
||||
|
||||
return createModal({
|
||||
component: Error,
|
||||
onClose: () => isProcessing.value = false,
|
||||
props: {
|
||||
error: response.error.message,
|
||||
canClose: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const db = new Database();
|
||||
await db.settings.add({
|
||||
name: "token",
|
||||
value: response.token,
|
||||
});
|
||||
await reloadAccount();
|
||||
|
||||
closeLoadingModal();
|
||||
|
||||
return createModal({
|
||||
component: Success,
|
||||
onClose: async () => {
|
||||
isProcessing.value = false;
|
||||
router.push("/");
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user