Feat: Service Workerの自動更新 / Chg: serviceWorker登録をVitePWAに任せないように / Chg: オフラインでも耐えられる?ように / Feat: Service Workerのキャッシュが全ページで効くように / Fix: ユーザーIDのautocompleteがuseridになっていた問題 / Enhance: パスワード強度チェッカーを改善

This commit is contained in:
2026-06-01 17:37:23 +09:00
parent fee45d5a31
commit 45a682b6ca
11 changed files with 71 additions and 32 deletions
+21 -3
View File
@@ -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(() => {
+9 -2
View File
@@ -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"]>([]);
+7 -1
View File
@@ -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;
+1 -1
View File
@@ -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("サーバー情報の取得に失敗しました。");
}
+2 -2
View File
@@ -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("サーバー情報の取得に失敗しました。");
}
+3 -3
View File
@@ -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("サーバー情報の取得に失敗しました。");
}
+1 -1
View File
@@ -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}"],