0
0
Fork 0
This commit is contained in:
Xeltica 2021-09-13 22:39:14 +09:00
parent dc6a53e38c
commit 587136a82d
11 changed files with 90 additions and 14 deletions

View file

@ -0,0 +1,23 @@
import { IsIn, IsOptional } from 'class-validator';
import { AlertMode, alertModes } from '../../common/types/alert-mode';
import { visibilities, Visibility } from '../../common/types/visibility';
export class UserSetting {
@IsIn(alertModes)
@IsOptional()
alertMode?: AlertMode;
@IsIn(visibilities)
@IsOptional()
visibility?: Visibility;
@IsOptional()
localOnly?: boolean;
@IsOptional()
remoteFollowersOnly?: boolean;
@IsOptional()
template?: string;
}

View file

@ -3,9 +3,13 @@
* @author Xeltica * @author Xeltica
*/ */
import { CurrentUser, Get, JsonController } from 'routing-controllers'; import { IsEnum } from 'class-validator';
import { Body, CurrentUser, Get, HttpCode, JsonController, OnUndefined, Post, Put } from 'routing-controllers';
import { DeepPartial } from 'typeorm';
import { getScores } from '../functions/get-scores'; import { getScores } from '../functions/get-scores';
import { updateUser } from '../functions/users';
import { User } from '../models/entities/user'; import { User } from '../models/entities/user';
import { UserSetting } from './UserSetting';
@JsonController('/session') @JsonController('/session')
export class SessionController { export class SessionController {
@ -17,4 +21,18 @@ export class SessionController {
async getScore(@CurrentUser({ required: true }) user: User) { async getScore(@CurrentUser({ required: true }) user: User) {
return getScores(user); return getScores(user);
} }
@OnUndefined(204)
@Put() async updateSetting(@CurrentUser({ required: true }) user: User, @Body() setting: UserSetting) {
const s: DeepPartial<User> = {};
if (setting.alertMode) s.alertMode = setting.alertMode;
if (setting.visibility) s.visibility = setting.visibility;
if (setting.localOnly) s.localOnly = setting.localOnly;
if (setting.remoteFollowersOnly) s.remoteFollowersOnly = setting.remoteFollowersOnly;
if (setting.template) s.template = setting.template;
if (Object.keys(s).length === 0) return;
await updateUser(user.username, user.host, s);
} }
}

View file

@ -23,7 +23,8 @@ export default (): void => {
useKoaServer(app, { useKoaServer(app, {
controllers: [__dirname + '/controllers/**/*{.ts,.js}'], controllers: [__dirname + '/controllers/**/*{.ts,.js}'],
routePrefix: '/api/v1', routePrefix: '/api/v1',
defaultErrorHandler: false, classTransformer: true,
validation: true,
currentUserChecker: async ({ request }: Action) => { currentUserChecker: async ({ request }: Action) => {
const { authorization } = request.header; const { authorization } = request.header;
if (!authorization || !authorization.startsWith('Bearer ')) return null; if (!authorization || !authorization.startsWith('Bearer ')) return null;
@ -35,6 +36,7 @@ export default (): void => {
}); });
app.use(router.routes()); app.use(router.routes());
app.use(router.allowedMethods());
app.keys = ['人類', 'ミス廃化', '計画', 'ここに極まれり', 'フッフッフ...']; app.keys = ['人類', 'ミス廃化', '計画', 'ここに極まれり', 'フッフッフ...'];

View file

@ -1,4 +1,5 @@
import React from 'react'; import React, { useEffect } from 'react';
import { LOCALSTORAGE_KEY_TOKEN } from '../const';
import { useGetScoreQuery, useGetSessionQuery } from '../services/session'; import { useGetScoreQuery, useGetSessionQuery } from '../services/session';
import { Skeleton } from './Skeleton'; import { Skeleton } from './Skeleton';
@ -6,6 +7,18 @@ export const SessionDataPage: React.VFC = () => {
const session = useGetSessionQuery(undefined); const session = useGetSessionQuery(undefined);
const score = useGetScoreQuery(undefined); const score = useGetScoreQuery(undefined);
/**
* Session APIのエラーハンドリング
* APIがエラーを返した =
*/
useEffect(() => {
if (session.error) {
console.error(session.error);
localStorage.removeItem(LOCALSTORAGE_KEY_TOKEN);
location.reload();
}
}, [session.error]);
return session.isLoading || score.isLoading ? ( return session.isLoading || score.isLoading ? (
<div className="vstack"> <div className="vstack">
<Skeleton width="100%" height="1rem" /> <Skeleton width="100%" height="1rem" />

View file

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useReducer, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
import { AlertMode } from '../../common/types/alert-mode'; import { AlertMode } from '../../common/types/alert-mode';
import { IUser } from '../../common/types/user'; import { IUser } from '../../common/types/user';
import { Visibility } from '../../common/types/visibility'; import { Visibility } from '../../common/types/visibility';
@ -6,6 +6,7 @@ import { useGetSessionQuery } from '../services/session';
import { defaultTemplate } from '../../common/default-template'; import { defaultTemplate } from '../../common/default-template';
import { Card } from './Card'; import { Card } from './Card';
import { Theme } from '../misc/theme'; import { Theme } from '../misc/theme';
import { API_ENDPOINT, LOCALSTORAGE_KEY_TOKEN } from '../const';
type SettingDraftType = Pick<IUser, type SettingDraftType = Pick<IUser,
| 'alertMode' | 'alertMode'
@ -50,6 +51,21 @@ export const SettingPage: React.VFC = () => {
const [currentTheme, setCurrentTheme] = useState<Theme>('light'); const [currentTheme, setCurrentTheme] = useState<Theme>('light');
const [currentLang, setCurrentLang] = useState<string>('ja-JP'); const [currentLang, setCurrentLang] = useState<string>('ja-JP');
const updateSetting = useCallback(() => {
fetch(`${API_ENDPOINT}session`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${localStorage[LOCALSTORAGE_KEY_TOKEN]}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(draft),
})
.then(() => alert('設定を保存しました。'))
.catch(e => {
alert(e.message);
});
}, [draft]);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
dispatchDraft({ dispatchDraft({
@ -60,13 +76,13 @@ export const SettingPage: React.VFC = () => {
template: data.template, template: data.template,
}); });
} }
}, [session.data]); }, [data]);
const saveButton = useMemo(() => ( const saveButton = useMemo(() => (
<button className="btn primary" style={{alignSelf: 'flex-end'}}> <button className="btn primary" style={{alignSelf: 'flex-end'}} onClick={updateSetting}>
</button> </button>
), []); ), [updateSetting]);
return session.isLoading || !data ? ( return session.isLoading || !data ? (
<div className="skeleton" style={{width: '100%', height: '128px'}}></div> <div className="skeleton" style={{width: '100%', height: '128px'}}></div>

3
src/frontend/const.ts Normal file
View file

@ -0,0 +1,3 @@
export const LOCALSTORAGE_KEY_TOKEN = 'token';
export const API_ENDPOINT = `//${location.host}/api/v1/`;

View file

@ -1 +0,0 @@
export const apiEndpoint = `//${location.host}/api/v1/`;

View file

@ -1,6 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom';
import { App } from './App'; import { App } from './App';
import { LOCALSTORAGE_KEY_TOKEN } from './const';
document.body.classList.add('dark'); document.body.classList.add('dark');
@ -11,7 +12,7 @@ const token = document.cookie
?.split('=')[1]; ?.split('=')[1];
if (token) { if (token) {
localStorage['token'] = token; localStorage[LOCALSTORAGE_KEY_TOKEN] = token;
// 今の所はcookieをトークン以外に使用しないため全消去する // 今の所はcookieをトークン以外に使用しないため全消去する
// もしcookieの用途が増えるのであればここを良い感じに書き直す必要がある // もしcookieの用途が増えるのであればここを良い感じに書き直す必要がある
document.cookie = ''; document.cookie = '';

0
src/frontend/misc/api.ts Normal file
View file

View file

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { LOCALSTORAGE_KEY_TOKEN } from '../const';
import { IndexSessionPage } from './index.session'; import { IndexSessionPage } from './index.session';
import { IndexWelcomePage } from './index.welcome'; import { IndexWelcomePage } from './index.welcome';
export const IndexPage: React.VFC = () => { export const IndexPage: React.VFC = () => {
const token = localStorage.getItem('token'); const token = localStorage[LOCALSTORAGE_KEY_TOKEN];
return token ? <IndexSessionPage /> : <IndexWelcomePage />; return token ? <IndexSessionPage /> : <IndexWelcomePage />;
}; };

View file

@ -1,17 +1,17 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { apiEndpoint } from '../const'; import { API_ENDPOINT, LOCALSTORAGE_KEY_TOKEN } from '../const';
import { IUser } from '../../common/types/user'; import { IUser } from '../../common/types/user';
import { Score } from '../../common/types/score'; import { Score } from '../../common/types/score';
export const sessionApi = createApi({ export const sessionApi = createApi({
reducerPath: 'session', reducerPath: 'session',
baseQuery: fetchBaseQuery({ baseUrl: apiEndpoint + 'session' }), baseQuery: fetchBaseQuery({ baseUrl: API_ENDPOINT + 'session' }),
endpoints: (builder) => ({ endpoints: (builder) => ({
getSession: builder.query<IUser, undefined>({ getSession: builder.query<IUser, undefined>({
query: () => ({ query: () => ({
url: '/', url: '/',
headers: { headers: {
'Authorization': `Bearer ${localStorage['token']}`, 'Authorization': `Bearer ${localStorage[LOCALSTORAGE_KEY_TOKEN]}`,
} }
}) })
}), }),
@ -19,7 +19,7 @@ export const sessionApi = createApi({
query: () => ({ query: () => ({
url: '/score', url: '/score',
headers: { headers: {
'Authorization': `Bearer ${localStorage['token']}`, 'Authorization': `Bearer ${localStorage[LOCALSTORAGE_KEY_TOKEN]}`,
} }
}) })
}), }),