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="ホーム"
|
title="ホーム"
|
||||||
>
|
>
|
||||||
<img :src='"icon" in serverInfo && serverInfo.icon
|
<img :src='serverInfo && "icon" in serverInfo && serverInfo.icon
|
||||||
? serverInfo.icon
|
? serverInfo.icon
|
||||||
: "/assets/lynqchat.svg"'
|
: "/assets/lynqchat.svg"'
|
||||||
/>
|
/>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<div class="content-header">
|
<div class="content-header">
|
||||||
{{
|
{{
|
||||||
title
|
title
|
||||||
?? (serverInfo.success
|
?? (serverInfo?.success
|
||||||
? serverInfo.name
|
? serverInfo.name
|
||||||
: null)
|
: null)
|
||||||
?? "LynqChat"
|
?? "LynqChat"
|
||||||
@@ -261,7 +261,7 @@ watch(route, () => {
|
|||||||
title.value = route.meta.title;
|
title.value = route.meta.title;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!serverInfo.value.success) {
|
if (!serverInfo.value?.success) {
|
||||||
throw new Error("サーバー情報の取得に失敗しました。");
|
throw new Error("サーバー情報の取得に失敗しました。");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +294,24 @@ function handleError(event: ErrorEvent | PromiseRejectionEvent) {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
window.addEventListener("error", handleError);
|
window.addEventListener("error", handleError);
|
||||||
window.addEventListener("unhandledrejection", 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(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|||||||
@@ -4,8 +4,15 @@ import { ref } from "vue";
|
|||||||
|
|
||||||
await initClient();
|
await initClient();
|
||||||
|
|
||||||
export let serverInfo = ref<ApiMap["server-info"]["response"]>(await client.value.request("server-info"));
|
let serverInfoDraft: ApiMap["server-info"]["response"] | null = null;
|
||||||
export let account = ref<ApiMap["me"]["response"]>(await client.value.request("me"));
|
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]>();
|
export let presentCommunity = ref<Extract<ApiMap["community/list"]["response"], { communitys: any }>["communitys"][number]>();
|
||||||
|
|
||||||
let communitys = ref<Extract<ApiMap["community/list"]["response"], { communitys: any }>["communitys"]>([]);
|
let communitys = ref<Extract<ApiMap["community/list"]["response"], { communitys: any }>["communitys"]>([]);
|
||||||
|
|||||||
@@ -50,7 +50,13 @@ swSelf.addEventListener("fetch", (event) => {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
event.respondWith((async () => {
|
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)
|
if (cached)
|
||||||
return cached;
|
return cached;
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ router.afterEach((to, from) => {
|
|||||||
|
|
||||||
let serverName = "LynqChat";
|
let serverName = "LynqChat";
|
||||||
|
|
||||||
if (serverInfo.value.success && serverInfo.value.name) {
|
if (serverInfo.value?.success && serverInfo.value.name) {
|
||||||
serverName = serverInfo.value.name;
|
serverName = serverInfo.value.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ import Message from "@/components/Message.vue";
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
if (!serverInfo.value.success) {
|
if (!serverInfo.value?.success) {
|
||||||
throw new Error("サーバー情報の取得に失敗しました。");
|
throw new Error("サーバー情報の取得に失敗しました。");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,8 +145,8 @@ if (!serverInfo.value.isInitialized) {
|
|||||||
router.replace("/setup/initialization");
|
router.replace("/setup/initialization");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!account.value.success) {
|
if (!account.value?.success) {
|
||||||
switch (account.value.error.bad) {
|
switch (account.value?.error.bad) {
|
||||||
case "client":
|
case "client":
|
||||||
router.replace("/signin");
|
router.replace("/signin");
|
||||||
break;
|
break;
|
||||||
@@ -270,7 +270,7 @@ const send = async (e: Event) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!account.value.success) {
|
if (!account.value?.success) {
|
||||||
isSending.value = false;
|
isSending.value = false;
|
||||||
createModal({
|
createModal({
|
||||||
component: ErrorModal,
|
component: ErrorModal,
|
||||||
@@ -336,7 +336,7 @@ provide("now", now);
|
|||||||
(async () => {
|
(async () => {
|
||||||
isProcessing.value = true;
|
isProcessing.value = true;
|
||||||
|
|
||||||
if (!serverInfo.value.success) {
|
if (!serverInfo.value?.success) {
|
||||||
throw new Error("サーバー情報の取得に失敗しました。");
|
throw new Error("サーバー情報の取得に失敗しました。");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ const isProcessing = ref<boolean>(false);
|
|||||||
|
|
||||||
const channels = ref<Extract<ApiMap["channel/list"]["response"], { channels: any }>["channels"]>();
|
const channels = ref<Extract<ApiMap["channel/list"]["response"], { channels: any }>["channels"]>();
|
||||||
|
|
||||||
if (!serverInfo.value.success) {
|
if (!serverInfo.value?.success) {
|
||||||
throw new Error("サーバー情報の取得に失敗しました。");
|
throw new Error("サーバー情報の取得に失敗しました。");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,8 +145,8 @@ if (!serverInfo.value.isInitialized) {
|
|||||||
router.replace("/setup/initialization");
|
router.replace("/setup/initialization");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!account.value.success) {
|
if (!account.value?.success) {
|
||||||
switch (account.value.error.bad) {
|
switch (account.value?.error.bad) {
|
||||||
case "client":
|
case "client":
|
||||||
router.replace("/signin");
|
router.replace("/signin");
|
||||||
break;
|
break;
|
||||||
@@ -158,7 +158,7 @@ if (!account.value.success) {
|
|||||||
(async () => {
|
(async () => {
|
||||||
isProcessing.value = true;
|
isProcessing.value = true;
|
||||||
|
|
||||||
if (!serverInfo.value.success) {
|
if (!serverInfo.value?.success) {
|
||||||
throw new Error("サーバー情報の取得に失敗しました。");
|
throw new Error("サーバー情報の取得に失敗しました。");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { useRouter } from "vue-router";
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
if (!account.value.success) {
|
if (!account.value?.success) {
|
||||||
switch (account.value.error.bad) {
|
switch (account.value?.error.bad) {
|
||||||
case "client":
|
case "client":
|
||||||
router.replace("/signin");
|
router.replace("/signin");
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<form novalidate @submit="submit">
|
<form novalidate @submit="submit">
|
||||||
<Input
|
<Input
|
||||||
label="ユーザーID"
|
label="ユーザーID"
|
||||||
autocomplete="userid"
|
autocomplete="username"
|
||||||
v-model="userid"
|
v-model="userid"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -50,9 +50,10 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="password-strength"
|
class="input-issue password-strength"
|
||||||
v-if="passwordStrength.message"
|
v-if="passwordStrength.message"
|
||||||
>
|
>
|
||||||
|
<Icon :icon="passwordStrength.icon" />
|
||||||
{{ passwordStrength.message }}
|
{{ passwordStrength.message }}
|
||||||
</span>
|
</span>
|
||||||
</InputPassword>
|
</InputPassword>
|
||||||
@@ -94,6 +95,10 @@ form {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin: auto 0;
|
margin: auto 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.password-strength {
|
||||||
|
color: v-bind(passwordStrengthColor);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -117,7 +122,7 @@ import { Icon } from "@iconify/vue";
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isProcessing = ref<boolean>(false);
|
const isProcessing = ref<boolean>(false);
|
||||||
|
|
||||||
if (!serverInfo.value.success) {
|
if (!serverInfo.value?.success) {
|
||||||
throw new Error("サーバー情報の取得に失敗しました。");
|
throw new Error("サーバー情報の取得に失敗しました。");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,19 +142,22 @@ const emailIssue = computed(() => getIssueFromPath("email", result.value));
|
|||||||
const passwordIssue = computed(() => getIssueFromPath("password", result.value));
|
const passwordIssue = computed(() => getIssueFromPath("password", result.value));
|
||||||
|
|
||||||
const passwordScores = [
|
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 passwordStrength = computed(() => {
|
||||||
const evaluation = zxcvbn(password.value);
|
const evaluation = zxcvbn(password.value);
|
||||||
return {
|
return {
|
||||||
message: passwordScores[evaluation.score],
|
message: passwordScores[evaluation.score]![0],
|
||||||
score: evaluation.score,
|
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 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({
|
const schema = z.object({
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ import Textarea from "@/components/Textarea.vue";
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isProcessing = ref<boolean>(false);
|
const isProcessing = ref<boolean>(false);
|
||||||
|
|
||||||
if (!serverInfo.value.success) {
|
if (!serverInfo.value?.success) {
|
||||||
throw new Error("サーバー情報の取得に失敗しました。");
|
throw new Error("サーバー情報の取得に失敗しました。");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<form novalidate @submit="submit">
|
<form novalidate @submit="submit">
|
||||||
<Input
|
<Input
|
||||||
label="ユーザーID"
|
label="ユーザーID"
|
||||||
autocomplete="off"
|
autocomplete="username"
|
||||||
v-model="userid"
|
v-model="userid"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -78,11 +78,11 @@ import z from "zod/v3";
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isProcessing = ref<boolean>(false);
|
const isProcessing = ref<boolean>(false);
|
||||||
|
|
||||||
if (account.value.success) {
|
if (account.value?.success) {
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!serverInfo.value.success) {
|
if (!serverInfo.value?.success) {
|
||||||
throw new Error("サーバー情報の取得に失敗しました。");
|
throw new Error("サーバー情報の取得に失敗しました。");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
strategies: "injectManifest",
|
strategies: "injectManifest",
|
||||||
srcDir: "./src/lib",
|
srcDir: "./src/lib",
|
||||||
filename: "sw.ts",
|
filename: "sw.ts",
|
||||||
injectRegister: "inline",
|
injectRegister: false,
|
||||||
manifest: false,
|
manifest: false,
|
||||||
injectManifest: {
|
injectManifest: {
|
||||||
globPatterns: ["**/*.{js,css,html,ico,png,svg}"],
|
globPatterns: ["**/*.{js,css,html,ico,png,svg}"],
|
||||||
|
|||||||
Reference in New Issue
Block a user