wip
This commit is contained in:
parent
dc6a53e38c
commit
587136a82d
11 changed files with 90 additions and 14 deletions
23
src/backend/controllers/UserSetting.ts
Normal file
23
src/backend/controllers/UserSetting.ts
Normal 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;
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 = ['人類', 'ミス廃化', '計画', 'ここに極まれり', 'フッフッフ...'];
|
||||||
|
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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
3
src/frontend/const.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const LOCALSTORAGE_KEY_TOKEN = 'token';
|
||||||
|
|
||||||
|
export const API_ENDPOINT = `//${location.host}/api/v1/`;
|
|
@ -1 +0,0 @@
|
||||||
export const apiEndpoint = `//${location.host}/api/v1/`;
|
|
|
@ -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
0
src/frontend/misc/api.ts
Normal 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 />;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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]}`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue