Skip to content

Commit

Permalink
Add LogViewer and Github OAuth
Browse files Browse the repository at this point in the history
  • Loading branch information
zyxkad committed Jan 17, 2024
1 parent 5affa0c commit aa4ef58
Show file tree
Hide file tree
Showing 10 changed files with 558 additions and 29 deletions.
10 changes: 1 addition & 9 deletions .github/ISSUE_TEMPLATE/CRASH-UPLOAD.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,6 @@ body:
render: bash
validations:
required: true
- type: textarea
id: log-files
attributes:
label: "日志文件"
description: 如果日志太长无法提交,请将日志文件拖放到这里
placeholder: 不要在这里粘贴日志
validations:
required: false
- type: dropdown
id: os
attributes:
Expand All @@ -66,4 +58,4 @@ body:
- 客户端
- 服务端
validations:
required: true
required: true
86 changes: 86 additions & 0 deletions docs/.vitepress/auth/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import axios from "axios"
import cookies from "js-cookie"

export { getAuthToken, redirectToAuth, onAuthDone }

const GITHUB_CLIID = "dc5a44e3614c1250afa9"

const GITHUB_AUTH_URL = "https://github.com/login/oauth/authorize"
const GITHUB_AUTH_TOKEN_URL = "https://api.crashmc.com/api/v1/gh_access_token/"
const GH_OAUTH_STATE_NAME = "_github_oauth_state"
const GH_OAUTH_TOKEN_NAME = "_github_oauth_token"

interface StateI {
randstr: string
redirect: string
}

function getAuthToken(): string | undefined {
return cookies.get(GH_OAUTH_TOKEN_NAME)
}

function redirectToAuth(afterAuth: string | URL, scope?: string) {
const randstr = btoa(
String.fromCodePoint(...crypto.getRandomValues(new Uint8Array(63))),
)
cookies.set(
GH_OAUTH_STATE_NAME,
JSON.stringify({
randstr: randstr,
redirect: afterAuth.toString(),
} as StateI),
{
secure: true,
expires: 10 / (60 * 24), // in days
sameSite: "strict",
},
)
const authURL = new URL(GITHUB_AUTH_URL)
authURL.searchParams.set("client_id", GITHUB_CLIID)
authURL.searchParams.set("state", randstr)
authURL.searchParams.set(
"redirect_uri",
new URL("/_auth_redirect.html", window.location.toString()).toString(),
)
if (scope) {
authURL.searchParams.set("scope", scope)
}
window.location.assign(authURL.toString())
}

async function onAuthDone(): Promise<string | null> {
const location = new URL(window.location.toString())
const code = location.searchParams.get("code")
const randstr = location.searchParams.get("state")
if (!code || !randstr) {
return null
}
var state: StateI
try {
state = JSON.parse(cookies.get(GH_OAUTH_STATE_NAME))
} catch (err) {
console.debug("Could not parse cookie", GH_OAUTH_STATE_NAME, err)
return null
}
if (state.randstr !== randstr) {
console.debug("Auth random state string not same, probably XSS attack")
return null
}
var token: string
try {
const resp = await axios.post<string>(
GITHUB_AUTH_TOKEN_URL,
"code=" + escape(code),
)
const data = new URLSearchParams(resp.data)
token = data.get("access_token")
} catch (err) {
console.error("auth failed:", err)
return
}
cookies.set(GH_OAUTH_TOKEN_NAME, token, {
secure: true,
expires: 1,
})
return state.redirect
}
71 changes: 52 additions & 19 deletions docs/.vitepress/theme/components/Analyzer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ interface SolutionMatch {
}
interface AnalysisResult {
file: string
filepath: string
error: JavaError
matched: SolutionMatch[]
viewlink: string
}
/**
Expand All @@ -52,6 +53,7 @@ class MemFile {
readonly data: Uint8Array
readonly path: string
text?: string // only present when it's a vaild text file
private blobURL?: string
constructor(data: Uint8Array, path: string) {
this.data = data
Expand All @@ -70,6 +72,19 @@ class MemFile {
}
return path
}
getBlobURL(): string {
if (!this.blobURL) {
if (!this.text) {
throw new Error("Cannot create blob for non-text file")
}
const blob = new Blob([this.text], {
type: "text/plain",
})
this.blobURL = URL.createObjectURL(blob)
}
return this.blobURL
}
}
class AnalysisError {
Expand Down Expand Up @@ -243,11 +258,12 @@ async function startAnalysis(files: File[]): Promise<boolean> {
} else {
console.debug("MCLA is not loaded")
}
if (
analysisResults.value.length === 0 &&
logAnalysis(file.text)
) {
throw fallbackAnalysisDoneErr
if (analysisResults.value.length === 0) {
return logAnalysis(file.text).then((ok) => {
if (ok) {
throw fallbackAnalysisDoneErr
}
})
}
}),
),
Expand Down Expand Up @@ -433,9 +449,12 @@ async function mclAnalysis(file: MemFile): Promise<void> {
(matched) =>
matched.length &&
analysisResults.value.push({
file: filepath,
filepath: filepath,
error: result.error,
matched: matched,
viewlink: `/log-viewer.html?type=blob&link=${escape(
file.getBlobURL(),
)}&name=${escape(filepath)}#L${result.error.lineNo}`,
}),
),
)
Expand Down Expand Up @@ -901,6 +920,16 @@ function showAnalysisResult(status, msg, result_url, status_msg) {
finishAnalysis(status, status_msg)
}
function umamiTrack(...args) {
if (window.umami) {
try {
umami.track(...args)
} catch (err) {
console.error("umami error:", err)
}
}
}
/**
* 结束分析。
* @param {string} status 分析状态。
Expand All @@ -915,41 +944,41 @@ function finishAnalysis(status: string, msg: string) {
switch (status) {
case "EmptyLogErr":
labelMsg.value = "未读取到支持的日志格式, 请尝试直接上传 .log / .txt 文件"
umami.track("Analysis Error", {
umamiTrack("Analysis Error", {
Status: "Unsupport_Log_File_Ext",
ErrMsg: msg,
})
break
case "ReadLogErr":
labelMsg.value = "Log 文件读取错误"
umami.track("Analysis Error", {
umamiTrack("Analysis Error", {
Status: "Cannot_Read_Log_File",
ErrMsg: msg,
})
break
case "UnzipErr":
labelMsg.value = "日志文件解压错误"
umami.track("Analysis Error", {
umamiTrack("Analysis Error", {
Status: "Cannot_Unzip_Log_File",
ErrMsg: msg,
})
break
case "EncryptedZipFile":
labelMsg.value = "不支持加密 zip 文件"
umami.track("Analysis Error", {
umamiTrack("Analysis Error", {
Status: "Cannot_Load_Encrypted_Log_File",
ErrMsg: msg,
})
break
case "Unrecord":
redirectMsg.value = "提交反馈"
umami.track("Unrecord Crash", {
umamiTrack("Unrecord Crash", {
Status: "Unrecord_Crash",
Launcher: launcher,
})
break
case "Success":
umami.track("Analysis Finish", {
umamiTrack("Analysis Finish", {
Status: "Analysis_Success",
Launcher: launcher,
CrashReason: msg,
Expand All @@ -961,7 +990,7 @@ function finishAnalysis(status: string, msg: string) {
"MCLA 分析器意外退出,请点击下方按钮前往 GitHub 反馈。"
redirectUrl.value = "https://github.com/kmcsr/mcla/issues/new"
redirectMsg.value = "提交反馈"
umami.track("Analysis Error", {
umamiTrack("Analysis Error", {
Status: "MCLA_Error",
Launcher: launcher,
CrashReason: msg,
Expand All @@ -970,7 +999,7 @@ function finishAnalysis(status: string, msg: string) {
case "UnexpectedError":
default:
labelMsg.value = "未知错误"
umami.track("Analysis Error", {
umamiTrack("Analysis Error", {
Status: "Unknown_Error",
Launcher: launcher,
})
Expand All @@ -990,7 +1019,7 @@ function redirectTo(url?: string, newTab?: boolean) {
}
} else {
labelMsg.value = "无法重定向到解决方案页面"
umami.track("Analysis Error", {
umamiTrack("Analysis Error", {
Status: "Cannot_Redirect_To_Resolution",
Launcher: launcher,
Target: url,
Expand Down Expand Up @@ -1066,7 +1095,9 @@ onUnmounted(() => {
:key="i"
class="analysis-result-item">
<h4>错误信息 {{ i + 1 }}</h4>
<span>{{ result.file }}:{{ result.error.lineNo }}</span>
<a :href="result.viewlink" target="_blank">
{{ result.filepath }}:{{ result.error.lineNo }}
</a>
<code class="result-parsed-error">
{{ result.error.class }}: {{ result.error.message }}
</code>
Expand Down Expand Up @@ -1244,9 +1275,11 @@ svg {
.result-parsed-error {
margin-top: 0.5rem;
color: var(--vp-c-text-1);
white-space-collapse: preserve;
text-wrap: nowrap;
overflow: auto;
text-wrap: pre-wrap;
overflow: hidden;
overflow-wrap: anywhere;
}
.result-matched-error-title > span {
Expand Down
62 changes: 62 additions & 0 deletions docs/.vitepress/theme/components/AuthRedirect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<script setup lang="ts">
import { ref, onMounted } from "vue"
import { onAuthDone } from "../../auth/github"
const REDIRECT_TIMEOUT_SEC = 3
const loading = ref(true)
const failed = ref(false)
const redirectTarget = ref(null)
const redirectLeft = ref(REDIRECT_TIMEOUT_SEC)
function startRedirectInterval(): void {
const intId = setInterval(() => {
const left = (redirectLeft.value -= 1)
if (left <= 0) {
clearInterval(intId)
window.location.replace(redirectTarget.value)
}
}, 1000)
}
onMounted(async () => {
let redirectTo = await onAuthDone()
if (!redirectTo) {
redirectTo = "/"
failed.value = true
}
loading.value = false
redirectTarget.value = redirectTo
startRedirectInterval()
})
</script>

<template>
<div class="box">
<div v-if="loading">登录中, 请稍后</div>
<div v-else-if="redirectLeft > 0">
<div v-if="failed" class="failed">登录失败.</div>
<div v-else class="success">登录成功!</div>
{{ redirectLeft }} 秒后跳转<span v-if="failed">到主页</span>
<br />
没有跳转? 点击<a :href="redirectTarget">这里</a>
</div>
<a href="/">返回主页</a>
</div>
</template>

<style scoped>
.box {
text-align: center;
}
.failed {
font-size: 1.2rem;
color: red;
}
.success {
font-size: 1.2rem;
color: green;
}
</style>

0 comments on commit aa4ef58

Please sign in to comment.