Feat: Service Workerの自動更新 / Chg: serviceWorker登録をVitePWAに任せないように / Chg: オフラインでも耐えられる?ように / Feat: Service Workerのキャッシュが全ページで効くように / Fix: ユーザーIDのautocompleteがuseridになっていた問題 / Enhance: パスワード強度チェッカーを改善
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
: ""'
|
||||
title="ホーム"
|
||||
>
|
||||
<img :src='"icon" in serverInfo && serverInfo.icon
|
||||
<img :src='serverInfo && "icon" in serverInfo && serverInfo.icon
|
||||
? serverInfo.icon
|
||||
: "/assets/lynqchat.svg"'
|
||||
/>
|
||||
@@ -56,7 +56,7 @@
|
||||
<div class="content-header">
|
||||
{{
|
||||
title
|
||||
?? (serverInfo.success
|
||||
?? (serverInfo?.success
|
||||
? serverInfo.name
|
||||
: null)
|
||||
?? "LynqChat"
|
||||
@@ -261,7 +261,7 @@ watch(route, () => {
|
||||
title.value = route.meta.title;
|
||||
});
|
||||
|
||||
if (!serverInfo.value.success) {
|
||||
if (!serverInfo.value?.success) {
|
||||
throw new Error("サーバー情報の取得に失敗しました。");
|
||||
}
|
||||
|
||||
@@ -294,6 +294,24 @@ function handleError(event: ErrorEvent | PromiseRejectionEvent) {
|
||||
onMounted(async () => {
|
||||
window.addEventListener("error", handleError);
|
||||
window.addEventListener("unhandledrejection", handleError);
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
try {
|
||||
const swFile = import.meta.env.MODE === "production"
|
||||
? "/sw.js"
|
||||
: "/dev-sw.js?dev-sw";
|
||||
const registration = await navigator.serviceWorker.register(swFile, {
|
||||
updateViaCache: "none",
|
||||
type: import.meta.env.MODE === "production"
|
||||
? "classic"
|
||||
: "module",
|
||||
scope: "/",
|
||||
});
|
||||
await registration.update();
|
||||
} catch (err) {
|
||||
console.error("Service Worker registration failed:", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -4,8 +4,15 @@ import { ref } from "vue";
|
||||
|
||||
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"));
|
||||
let serverInfoDraft: ApiMap["server-info"]["response"] | null = null;
|
||||
let accountDraft: ApiMap["me"]["response"] | null = null;
|
||||
try {
|
||||
serverInfoDraft = await client.value.request("server-info");
|
||||
accountDraft = await client.value.request("me");
|
||||
} catch {}
|
||||
|
||||
export let serverInfo = ref<ApiMap["server-info"]["response"] | null>(serverInfoDraft);
|
||||
export let account = ref<ApiMap["me"]["response"] | null>(accountDraft);
|
||||
export let presentCommunity = ref<Extract<ApiMap["community/list"]["response"], { communitys: any }>["communitys"][number]>();
|
||||
|
||||
let communitys = ref<Extract<ApiMap["community/list"]["response"], { communitys: any }>["communitys"]>([]);
|
||||
|
||||
@@ -50,7 +50,13 @@ swSelf.addEventListener("fetch", (event) => {
|
||||
return;
|
||||
|
||||
event.respondWith((async () => {
|
||||
const cached = await caches.match(request);
|
||||
let cached: Response | undefined;
|
||||
if (request.destination === "document") {
|
||||
cached = await caches.match("/index.html");
|
||||
} else {
|
||||
cached = await caches.match(request);
|
||||
}
|
||||
|
||||
if (cached)
|
||||
return cached;
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ router.afterEach((to, from) => {
|
||||
|
||||
let serverName = "LynqChat";
|
||||
|
||||
if (serverInfo.value.success && serverInfo.value.name) {
|
||||
if (serverInfo.value?.success && serverInfo.value.name) {
|
||||
serverName = serverInfo.value.name;
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ import Message from "@/components/Message.vue";
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
if (!serverInfo.value.success) {
|
||||
if (!serverInfo.value?.success) {
|
||||
throw new Error("サーバー情報の取得に失敗しました。");
|
||||
}
|
||||
|
||||
@@ -145,8 +145,8 @@ if (!serverInfo.value.isInitialized) {
|
||||
router.replace("/setup/initialization");
|
||||
}
|
||||
|
||||
if (!account.value.success) {
|
||||
switch (account.value.error.bad) {
|
||||
if (!account.value?.success) {
|
||||
switch (account.value?.error.bad) {
|
||||
case "client":
|
||||
router.replace("/signin");
|
||||
break;
|
||||
@@ -270,7 +270,7 @@ const send = async (e: Event) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!account.value.success) {
|
||||
if (!account.value?.success) {
|
||||
isSending.value = false;
|
||||
createModal({
|
||||
component: ErrorModal,
|
||||
@@ -336,7 +336,7 @@ provide("now", now);
|
||||
(async () => {
|
||||
isProcessing.value = true;
|
||||
|
||||
if (!serverInfo.value.success) {
|
||||
if (!serverInfo.value?.success) {
|
||||
throw new Error("サーバー情報の取得に失敗しました。");
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ const isProcessing = ref<boolean>(false);
|
||||
|
||||
const channels = ref<Extract<ApiMap["channel/list"]["response"], { channels: any }>["channels"]>();
|
||||
|
||||
if (!serverInfo.value.success) {
|
||||
if (!serverInfo.value?.success) {
|
||||
throw new Error("サーバー情報の取得に失敗しました。");
|
||||
}
|
||||
|
||||
@@ -145,8 +145,8 @@ if (!serverInfo.value.isInitialized) {
|
||||
router.replace("/setup/initialization");
|
||||
}
|
||||
|
||||
if (!account.value.success) {
|
||||
switch (account.value.error.bad) {
|
||||
if (!account.value?.success) {
|
||||
switch (account.value?.error.bad) {
|
||||
case "client":
|
||||
router.replace("/signin");
|
||||
break;
|
||||
@@ -158,7 +158,7 @@ if (!account.value.success) {
|
||||
(async () => {
|
||||
isProcessing.value = true;
|
||||
|
||||
if (!serverInfo.value.success) {
|
||||
if (!serverInfo.value?.success) {
|
||||
throw new Error("サーバー情報の取得に失敗しました。");
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
if (!account.value.success) {
|
||||
switch (account.value.error.bad) {
|
||||
if (!account.value?.success) {
|
||||
switch (account.value?.error.bad) {
|
||||
case "client":
|
||||
router.replace("/signin");
|
||||
break;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<form novalidate @submit="submit">
|
||||
<Input
|
||||
label="ユーザーID"
|
||||
autocomplete="userid"
|
||||
autocomplete="username"
|
||||
v-model="userid"
|
||||
>
|
||||
<span
|
||||
@@ -50,9 +50,10 @@
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="password-strength"
|
||||
class="input-issue password-strength"
|
||||
v-if="passwordStrength.message"
|
||||
>
|
||||
<Icon :icon="passwordStrength.icon" />
|
||||
{{ passwordStrength.message }}
|
||||
</span>
|
||||
</InputPassword>
|
||||
@@ -94,6 +95,10 @@ form {
|
||||
font-size: 1rem;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.password-strength {
|
||||
color: v-bind(passwordStrengthColor);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -117,7 +122,7 @@ import { Icon } from "@iconify/vue";
|
||||
const router = useRouter();
|
||||
const isProcessing = ref<boolean>(false);
|
||||
|
||||
if (!serverInfo.value.success) {
|
||||
if (!serverInfo.value?.success) {
|
||||
throw new Error("サーバー情報の取得に失敗しました。");
|
||||
}
|
||||
|
||||
@@ -137,19 +142,22 @@ const emailIssue = computed(() => getIssueFromPath("email", result.value));
|
||||
const passwordIssue = computed(() => getIssueFromPath("password", result.value));
|
||||
|
||||
const passwordScores = [
|
||||
"危険なパスワード",
|
||||
"推測可能なパスワード",
|
||||
"推測しやすいパスワード",
|
||||
"安全なパスワード",
|
||||
"強力なパスワード",
|
||||
["危険なパスワード", "error-outline-rounded", "var(--error-color)"],
|
||||
["推測可能なパスワード", "warning-rounded", "var(--warn-color)"],
|
||||
["推測しやすいパスワード", "warning-rounded", "var(--warn-color)"],
|
||||
["安全なパスワード", "check-circle", "var(--success-color)"],
|
||||
["強力なパスワード", "check-circle", "var(--success-color)"],
|
||||
];
|
||||
const passwordStrength = computed(() => {
|
||||
const evaluation = zxcvbn(password.value);
|
||||
return {
|
||||
message: passwordScores[evaluation.score],
|
||||
message: passwordScores[evaluation.score]![0],
|
||||
score: evaluation.score,
|
||||
icon: "material-symbols:" + passwordScores[evaluation.score]![1],
|
||||
color: passwordScores[evaluation.score]![2],
|
||||
}
|
||||
});
|
||||
const passwordStrengthColor = computed(() => passwordStrength.value.color);
|
||||
|
||||
const EmailRegex = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:([0-9]{1,3}\.){3}[0-9]{1,3})\])$/i;
|
||||
const schema = z.object({
|
||||
|
||||
@@ -106,7 +106,7 @@ import Textarea from "@/components/Textarea.vue";
|
||||
const router = useRouter();
|
||||
const isProcessing = ref<boolean>(false);
|
||||
|
||||
if (!serverInfo.value.success) {
|
||||
if (!serverInfo.value?.success) {
|
||||
throw new Error("サーバー情報の取得に失敗しました。");
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<form novalidate @submit="submit">
|
||||
<Input
|
||||
label="ユーザーID"
|
||||
autocomplete="off"
|
||||
autocomplete="username"
|
||||
v-model="userid"
|
||||
>
|
||||
<span
|
||||
@@ -78,11 +78,11 @@ import z from "zod/v3";
|
||||
const router = useRouter();
|
||||
const isProcessing = ref<boolean>(false);
|
||||
|
||||
if (account.value.success) {
|
||||
if (account.value?.success) {
|
||||
router.replace("/");
|
||||
}
|
||||
|
||||
if (!serverInfo.value.success) {
|
||||
if (!serverInfo.value?.success) {
|
||||
throw new Error("サーバー情報の取得に失敗しました。");
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export default defineConfig(({ mode }) => {
|
||||
strategies: "injectManifest",
|
||||
srcDir: "./src/lib",
|
||||
filename: "sw.ts",
|
||||
injectRegister: "inline",
|
||||
injectRegister: false,
|
||||
manifest: false,
|
||||
injectManifest: {
|
||||
globPatterns: ["**/*.{js,css,html,ico,png,svg}"],
|
||||
|
||||
Reference in New Issue
Block a user