-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat(server): support iaaa provider * feat: finished iaaa * chore: bump versions
- Loading branch information
Showing
13 changed files
with
272 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
releases: | ||
"@aoi-js/frontend": patch | ||
"@aoi-js/server": patch |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
<template> | ||
<VCardText class="text-center"> | ||
<VProgressCircular indeterminate /> | ||
<p>{{ t('iaaa-wait') }}</p> | ||
</VCardText> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import { useAsyncTask, sleep } from '@/utils/async' | ||
import { http, login } from '@/utils/http' | ||
import { HTTPError } from 'ky' | ||
import { onMounted } from 'vue' | ||
import { useI18n } from 'vue-i18n' | ||
import { useRouter } from 'vue-router' | ||
import { useToast } from 'vue-toastification' | ||
const { t } = useI18n() | ||
const router = useRouter() | ||
const toast = useToast() | ||
const html = ` | ||
<form action=https://iaaa.pku.edu.cn/iaaa/oauth.jsp method=post name=iaaa style="display: none"> | ||
<input type=hidden name=appID value="${import.meta.env.VITE_IAAA_APPID}" /> | ||
<input type=hidden name=appName value="${import.meta.env.VITE_IAAA_APPNAME}" /> | ||
<input type=hidden name=redirectUrl value="${import.meta.env.VITE_IAAA_REDIR}" /> | ||
</form> | ||
` | ||
async function getToken() { | ||
const win = window.open('/auth/iaaa', 'iaaa', 'width=800,height=600') | ||
const client = win?.window | ||
if (!client) throw new Error('Failed to open IAAA window') | ||
client.document.write(html) | ||
client.document.forms[0].submit() | ||
for (;;) { | ||
try { | ||
const token = new URLSearchParams(client.document.location.search).get('token') | ||
if (token) { | ||
win.close() | ||
return token | ||
} | ||
} catch (err) { | ||
// | ||
} | ||
await sleep(200) | ||
} | ||
} | ||
const task = useAsyncTask(async () => { | ||
try { | ||
const resp = await http.post('auth/login', { | ||
json: { | ||
provider: 'iaaa', | ||
payload: { | ||
token: await getToken() | ||
} | ||
} | ||
}) | ||
const { token } = await resp.json<{ token: string }>() | ||
toast.success(t('hint.signin-success')) | ||
login(token) | ||
router.replace('/') | ||
} catch (err) { | ||
router.replace('/login') | ||
let msg = `${err}` | ||
if (err instanceof HTTPError) { | ||
msg = await err.response | ||
.json() | ||
.then(({ message }) => message) | ||
.catch((err) => `${err}`) | ||
} | ||
throw new Error(msg) | ||
} | ||
}) | ||
onMounted(() => { | ||
task.execute() | ||
}) | ||
</script> | ||
|
||
<i18n> | ||
en: | ||
iaaa-wait: Waiting for IAAA login... | ||
zh-Hans: | ||
iaaa-wait: 等待 IAAA 登录... | ||
</i18n> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { Type } from '@sinclair/typebox' | ||
import { loadEnv, users } from '../index.js' | ||
import { BaseAuthProvider } from './base.js' | ||
import { TypeCompiler } from '@sinclair/typebox/compiler' | ||
import { UUID } from 'mongodb' | ||
import { httpErrors } from '@fastify/sensible' | ||
import { validate } from '@lcpu/iaaa' | ||
import { FastifyRequest } from 'fastify' | ||
|
||
const STokenPayload = Type.Object({ | ||
token: Type.String() | ||
}) | ||
|
||
const TokenPayload = TypeCompiler.Compile(STokenPayload) | ||
|
||
export class IaaaAuthProvider extends BaseAuthProvider { | ||
private iaaaId = '' | ||
private iaaaKey = '' | ||
private allowSignupFromLogin = false | ||
|
||
constructor() { | ||
super() | ||
} | ||
|
||
override readonly name = 'iaaa' | ||
|
||
override async init(): Promise<void> { | ||
this.iaaaId = loadEnv('IAAA_ID', String) | ||
this.iaaaKey = loadEnv('IAAA_KEY', String) | ||
this.allowSignupFromLogin = loadEnv( | ||
'IAAA_ALLOW_SIGNUP_FROM_LOGIN', | ||
(x) => !!JSON.parse(x), | ||
false | ||
) | ||
|
||
await users.createIndex( | ||
{ 'authSources.iaaaId': 1 }, | ||
{ unique: true, partialFilterExpression: { 'authSources.iaaaId': { $exists: true } } } | ||
) | ||
} | ||
|
||
override async bind(userId: UUID, payload: unknown, req: FastifyRequest): Promise<unknown> { | ||
if (!TokenPayload.Check(payload)) throw httpErrors.badRequest() | ||
const resp = await validate(req.ip, this.iaaaId, this.iaaaKey, payload.token) | ||
if (!resp.success) throw httpErrors.forbidden(resp.errMsg) | ||
await users.updateOne( | ||
{ _id: userId }, | ||
{ | ||
$set: { | ||
'profile.realname': resp.userInfo.name, | ||
'profile.school': '北京大学', | ||
'profile.studentGrade': `${resp.userInfo.dept}${resp.userInfo.detailType}(${resp.userInfo.campus})`, | ||
'authSources.iaaaId': resp.userInfo.identityId | ||
}, | ||
$addToSet: { 'profile.verified': ['realname', 'school', 'studentGrade'] } | ||
} | ||
) | ||
return {} | ||
} | ||
|
||
override async verify(userId: UUID, payload: unknown, req: FastifyRequest): Promise<boolean> { | ||
if (!TokenPayload.Check(payload)) throw httpErrors.badRequest() | ||
const resp = await validate(req.ip, this.iaaaId, this.iaaaKey, payload.token) | ||
if (!resp.success) throw httpErrors.forbidden(resp.errMsg) | ||
const user = await users.findOne({ _id: userId }, { projection: { 'authSources.iaaaId': 1 } }) | ||
if (!user) throw httpErrors.notFound('User not found') | ||
if (!user.authSources.iaaaId) throw httpErrors.forbidden('User has no IAAA ID') | ||
if (user.authSources.iaaaId !== resp.userInfo.identityId) | ||
throw httpErrors.forbidden('Invalid IAAA ID') | ||
return true | ||
} | ||
|
||
override async login( | ||
payload: unknown, | ||
req: FastifyRequest | ||
): Promise<[userId: UUID, tags?: string[]]> { | ||
if (!TokenPayload.Check(payload)) throw httpErrors.badRequest() | ||
const resp = await validate(req.ip, this.iaaaId, this.iaaaKey, payload.token) | ||
if (!resp.success) throw httpErrors.forbidden(resp.errMsg) | ||
let userId: UUID | ||
const user = await users.findOne( | ||
{ 'authSources.iaaaId': resp.userInfo.identityId }, | ||
{ projection: { _id: 1 } } | ||
) | ||
if (user) { | ||
userId = user._id | ||
} else { | ||
if (!this.allowSignupFromLogin) throw httpErrors.notFound('User not found') | ||
const { insertedId } = await users.insertOne({ | ||
_id: new UUID(), | ||
profile: { | ||
name: resp.userInfo.identityId, | ||
email: `${resp.userInfo.identityId}@pku.edu.cn`, | ||
realname: resp.userInfo.name, | ||
school: '北京大学', | ||
studentGrade: `${resp.userInfo.dept}${resp.userInfo.detailType}(${resp.userInfo.campus})`, | ||
verified: ['realname', 'school', 'studentGrade'] | ||
}, | ||
authSources: { | ||
iaaaId: resp.userInfo.identityId | ||
} | ||
}) | ||
userId = insertedId | ||
} | ||
return [userId] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.