diff --git a/src/backend/die.ts b/src/backend/die.ts index 4b26e22..b3844ca 100644 --- a/src/backend/die.ts +++ b/src/backend/die.ts @@ -1,6 +1,7 @@ import { Context } from 'koa'; +import { ErrorCode } from '../common/types/error-code'; -export const die = (ctx: Context, error = '問題が発生しました。お手数ですが、最初からやり直してください。', status = 400): Promise => { +export const die = (ctx: Context, error: ErrorCode = 'other', status = 400): Promise => { ctx.status = status; - return ctx.render('error', { error }); + return ctx.render('frontend', { error }); }; diff --git a/src/backend/router.ts b/src/backend/router.ts index 6245092..7890ff9 100644 --- a/src/backend/router.ts +++ b/src/backend/router.ts @@ -7,13 +7,8 @@ import { v4 as uuid } from 'uuid'; import ms from 'ms'; import { config } from '../config'; -import { upsertUser, getUser, updateUser, updateUsersMisshaiToken, getUserByMisshaiToken, deleteUser } from './functions/users'; +import { upsertUser, getUser, updateUser, updateUsersMisshaiToken } from './functions/users'; import { api } from './services/misskey'; -import { AlertMode, alertModes } from '../common/types/alert-mode'; -import { Users } from './models'; -import { sendAlert } from './services/send-alert'; -import { visibilities, Visibility } from '../common/types/visibility'; -import { defaultTemplate } from '../common/default-template'; import { die } from './die'; export const router = new Router(); @@ -23,15 +18,14 @@ const tokenSecretCache: Record = {}; router.get('/login', async ctx => { let host = ctx.query.host as string | undefined; - if (!host) { - await die(ctx, 'host is empty'); + await die(ctx, 'invalidParamater'); return; } - const meta = await api<{ name: string, uri: string, version: string, features: Record }>(host, 'meta', {}); + const meta = await api<{ name: string, uri: string, version: string, features: Record }>(host, 'meta', {}); if (meta.version.includes('hitori')) { - await die(ctx, 'ひとりすきーは連携できません。'); + await die(ctx, 'hitorisskeyIsDenied'); return; } @@ -42,7 +36,7 @@ router.get('/login', async ctx => { const permission = ['write:notes', 'write:notifications']; if (meta.features.miauth) { - // Use MiAuth + // MiAuthを使用する const callback = encodeURI(`${config.url}/miauth`); const session = uuid(); @@ -51,7 +45,7 @@ router.get('/login', async ctx => { ctx.redirect(url); } else { - // Use legacy authentication + // 旧型認証を使用する const callbackUrl = encodeURI(`${config.url}/legacy-auth`); const { secret } = await api<{ secret: string }>(host, 'app/create', { @@ -70,13 +64,13 @@ router.get('/login', async ctx => { }); router.get('/teapot', async ctx => { - await die(ctx, 'I\'m a teapot', 418); + await die(ctx, 'teapot', 418); }); router.get('/miauth', async ctx => { const session = ctx.query.session as string | undefined; if (!session) { - await die(ctx, 'session required'); + await die(ctx, 'sessionRequired'); return; } const host = sessionHostCache[session]; @@ -101,7 +95,7 @@ router.get('/miauth', async ctx => { router.get('/legacy-auth', async ctx => { const token = ctx.query.token as string | undefined; if (!token) { - await die(ctx, 'token required'); + await die(ctx, 'tokenRequired'); return; } const host = sessionHostCache[token]; @@ -125,97 +119,6 @@ router.get('/legacy-auth', async ctx => { await login(ctx, user, host, i); }); -router.post('/update-settings', async ctx => { - const mode = ctx.request.body.alertMode as AlertMode; - // 一応型チェック - if (!alertModes.includes(mode)) { - await die(ctx, `${mode} is an invalid value`); - return; - } - const visibility = ctx.request.body.visibility as Visibility; - // 一応型チェック - if (!visibilities.includes(visibility)) { - await die(ctx, `${mode} is an invalid value`); - return; - } - - const flag = ctx.request.body.flag; - - const template = ctx.request.body.template?.trim(); - - const token = ctx.cookies.get('token'); - if (!token) { - await die(ctx, 'ログインしていません'); - return; - } - - const u = await getUserByMisshaiToken(token); - - if (!u) { - await die(ctx); - return; - } - - await Users.update(u.id, { - alertMode: mode, - localOnly: flag === 'localOnly', - remoteFollowersOnly: flag === 'remoteFollowersOnly', - template: template === defaultTemplate || !template ? null : template, - visibility, - }); - - ctx.redirect('/?from=updateSettings'); -}); - - - -router.post('/logout', async ctx => { - const token = ctx.cookies.get('token'); - if (!token) { - await die(ctx, 'ログインしていません'); - return; - } - ctx.cookies.set('token', ''); - ctx.redirect('/?from=logout'); -}); - -router.post('/optout', async ctx => { - const token = ctx.cookies.get('token'); - if (!token) { - await die(ctx, 'ログインしていません'); - return; - } - ctx.cookies.set('token', ''); - - const u = await getUserByMisshaiToken(token); - - if (!u) { - await die(ctx); - return; - } - - await deleteUser(u.username, u.host); - - ctx.redirect('/?from=optout'); -}); - -router.post('/send', async ctx => { - const token = ctx.cookies.get('token'); - if (!token) { - await die(ctx, 'ログインしていません'); - return; - } - - const u = await getUserByMisshaiToken(token); - - if (!u) { - await die(ctx); - return; - } - await sendAlert(u).catch(() => die(ctx)); - ctx.redirect('/?from=send'); -}); - router.get('/assets/(.*)', async ctx => { await koaSend(ctx as any, ctx.path.replace('/assets/', ''), { root: `${__dirname}/../assets/`, @@ -252,8 +155,5 @@ async function login(ctx: Context, user: Record, host: string, const misshaiToken = await updateUsersMisshaiToken(u); - ctx.cookies.set('token', misshaiToken, { signed: false, httpOnly: false }); - - // await ctx.render('logined', { user: u }); - ctx.redirect('/'); + await ctx.render('frontend', { token: misshaiToken }); } diff --git a/src/backend/views/frontend.pug b/src/backend/views/frontend.pug index d5d80b2..22356d7 100644 --- a/src/backend/views/frontend.pug +++ b/src/backend/views/frontend.pug @@ -29,4 +29,13 @@ html body #app: .loading Loading... + if token + script. + localStorage.setItem('token', '#{token}'); + history.replaceState(null, null, '/'); + + if error + script. + window.__misshaialert = { error: '#{error}' }; + script(src=`/assets/fe.${version}.js` async defer) diff --git a/src/common/types/error-code.ts b/src/common/types/error-code.ts new file mode 100644 index 0000000..dc0ec26 --- /dev/null +++ b/src/common/types/error-code.ts @@ -0,0 +1,11 @@ +export const errorCodes = [ + 'hitorisskeyIsDenied', + 'teapot', + 'sessionRequired', + 'tokenRequired', + 'invalidParamater', + 'notAuthorized', + 'other', +] as const; + +export type ErrorCode = typeof errorCodes[number]; diff --git a/src/frontend/App.tsx b/src/frontend/App.tsx index 2e96e98..f8852ed 100644 --- a/src/frontend/App.tsx +++ b/src/frontend/App.tsx @@ -9,10 +9,11 @@ import { Header } from './components/Header'; import { TermPage } from './pages/term'; import { store, useSelector } from './store'; import { ModalComponent } from './Modal'; +import { ActualTheme } from './misc/theme'; +import { ErrorCode } from '../common/types/error-code'; import 'xeltica-ui/dist/css/xeltica-ui.min.css'; import './style.scss'; -import { ActualTheme } from './misc/theme'; const AppInner : React.VFC = () => { const $location = useLocation(); @@ -49,22 +50,35 @@ const AppInner : React.VFC = () => { const {t} = useTranslation(); - return ( - <> -
- {$location.pathname !== '/' &&
} - - - - - -
-

(C)2020-2021 Xeltica

-

{t('termsOfService')}

-
- + const error = (window as any).__misshaialert?.error; + + return error ? ( +
+
+
+

{t('error')}

+

{t('_error.sorry')}

+

+ {t('_error.additionalInfo')} + {t(`_error.${error}`)} +

+ (window as any).__misshaialert.error = null}>{t('retry')}
- +
+ ) : ( +
+ {$location.pathname !== '/' &&
} + + + + + +
+

(C)2020-2021 Xeltica

+

{t('termsOfService')}

+
+ +
); }; diff --git a/src/frontend/components/Header.tsx b/src/frontend/components/Header.tsx index 618b7bb..8c7fcd7 100644 --- a/src/frontend/components/Header.tsx +++ b/src/frontend/components/Header.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import { welcomeMessage } from '../misc/welcome-message'; import { useTranslation } from 'react-i18next'; diff --git a/src/frontend/components/SettingPage.tsx b/src/frontend/components/SettingPage.tsx index 83d5c9c..df2e02e 100644 --- a/src/frontend/components/SettingPage.tsx +++ b/src/frontend/components/SettingPage.tsx @@ -121,7 +121,16 @@ export const SettingPage: React.VFC = () => { const onClickLogout = useCallback(() => { dispatch(showModal({ type: 'dialog', - message: 'WIP', + title: 'ログアウトしてもよろしいですか?', + message: 'ログアウトしても、アラート送信や、お使いのMisskeyアカウントのデータ収集といった機能は動作し続けます。Misskey Toolsの利用を停止したい場合は、「アカウント連携を解除する」ボタンを押下して下さい。', + icon: 'question', + buttons: 'yesNo', + onSelect(i) { + if (i === 0) { + localStorage.removeItem(LOCALSTORAGE_KEY_TOKEN); + location.reload(); + } + }, })); }, [dispatch]); @@ -149,33 +158,36 @@ export const SettingPage: React.VFC = () => { )) } - {draft.alertMode === 'notification' && ( -
- - {t('_alertMode.notificationWarning')} -
- )}
+ + { draft.alertMode === 'notification' && ( +
+ + {t('_alertMode.notificationWarning')} +
+ )} { draft.alertMode === 'note' && ( -
- - { - availableVisibilities.map((visibility) => ( - - )) - } + <> +

{t('visibility')}

+
+ { + availableVisibilities.map((visibility) => ( + + )) + } +
-
+ )} @@ -218,7 +230,7 @@ export const SettingPage: React.VFC = () => {
- + diff --git a/src/frontend/init.tsx b/src/frontend/init.tsx index a1f3a80..b8fefe8 100644 --- a/src/frontend/init.tsx +++ b/src/frontend/init.tsx @@ -4,31 +4,16 @@ import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import { App } from './App'; -import { LOCALSTORAGE_KEY_TOKEN } from './const'; -import { resources } from './langs'; +import { getBrowserLanguage, resources } from './langs'; document.body.classList.add('dark'); -// cookieにトークンが入ってたらlocalStorageに移し替える -const token = document.cookie - .split('; ') - .find(row => row.startsWith('token')) - ?.split('=')[1]; - -if (token) { - localStorage[LOCALSTORAGE_KEY_TOKEN] = token; - // 今の所はcookieをトークン以外に使用しないため全消去する - // もしcookieの用途が増えるのであればここを良い感じに書き直す必要がある - document.cookie = ''; -} - -console.log(resources); i18n .use(initReactI18next) .init({ resources, - lng: localStorage['lang'] ?? 'ja_JP', + lng: localStorage['lang'] ?? getBrowserLanguage(), interpolation: { escapeValue: false // react already safes from xss } diff --git a/src/frontend/langs/en_US.json5 b/src/frontend/langs/en_US.json5 index 3087fed..6a12d14 100644 --- a/src/frontend/langs/en_US.json5 +++ b/src/frontend/langs/en_US.json5 @@ -29,11 +29,14 @@ failedToFetch: "Failed to fetch", registeredUsersCount: "Users", isCalculating: "It is being calculated now. Please check back later!", + retry: "Try again", ok: "OK", yes: "Yes", no: "No", termsOfService: "Terms of Service", name: "Name", + resetToDefault: "Reset to default", + error: "Error", _welcomeMessage: { pattern1: "Overnoting Misskey?", pattern2: "Overusing Misskey?", @@ -92,8 +95,19 @@ system: "Follows System Preferences", }, _template: { - description: "アラートの自動投稿をカスタマイズできます。", - description2: "ハッシュタグ #misshaialert は、テンプレートに関わらず自動付与されます。", + description: "Customize template of your alert.", + description2: "Hashtag '#misshaialert' will be appended regardless of the template.", + }, + _error: { + sorry: "Something went wrong. Please retry again.", + additionalInfo: "Additional Info: ", + hitorisskeyIsDenied: "You cannot integrate with hitorisskey.", + teapot: "I'm a teapot.", + sessionRequired: "Session is required.", + tokenRequired: "Token is required.", + invalidParameter: "Invalid parameter.", + notAuthorized: "Not authorized.", + other: "None", }, }, } diff --git a/src/frontend/langs/index.ts b/src/frontend/langs/index.ts index 9ef25a0..bec79dc 100644 --- a/src/frontend/langs/index.ts +++ b/src/frontend/langs/index.ts @@ -9,4 +9,11 @@ export const resources = { export const languageName = { 'en_US': 'English', 'ja_JP': '日本語', +} as const; + +export type LanguageCode = keyof typeof resources; + +export const getBrowserLanguage = () => { + const lang = navigator.language; + return (Object.keys(resources) as LanguageCode[]).find(k => k.startsWith(lang)) ?? 'en_US'; }; diff --git a/src/frontend/langs/ja_JP.json5 b/src/frontend/langs/ja_JP.json5 index b443c55..1552860 100644 --- a/src/frontend/langs/ja_JP.json5 +++ b/src/frontend/langs/ja_JP.json5 @@ -34,6 +34,9 @@ no: "いいえ", termsOfService: "利用規約", name: "名前", + resetToDefault: "初期値に戻す", + error: "エラー", + retry: "やり直す", _welcomeMessage: { 'pattern1': 'ついついノートしすぎていませんか?', 'pattern2': 'Misskey, しすぎていませんか?', @@ -95,5 +98,16 @@ description: "アラートの自動投稿をカスタマイズできます。", description2: "ハッシュタグ #misshaialert は、テンプレートに関わらず自動付与されます。", }, + _error: { + sorry: "問題が発生しました。お手数ですが、やり直してください。", + additionalInfo: "追加情報: ", + hitorisskeyIsDenied: "ひとりすきーは連携できません。", + teapot: "I'm a teapot.", + sessionRequired: "セッションがありません。", + tokenRequired: "トークンがありません。", + invalidParameter: "パラメータが不正です。", + notAuthorized: "権限がありません。", + other: "なし", + }, }, } diff --git a/src/frontend/pages/index.session.tsx b/src/frontend/pages/index.session.tsx index 0ff372f..47620be 100644 --- a/src/frontend/pages/index.session.tsx +++ b/src/frontend/pages/index.session.tsx @@ -7,6 +7,7 @@ import { Tab, TabItem } from '../components/Tab'; import { SettingPage } from '../components/SettingPage'; import { useTranslation } from 'react-i18next'; + export const IndexSessionPage: React.VFC = () => { const [selectedTab, setSelectedTab] = useState(0); const {t, i18n} = useTranslation();