commit ac49a787e567ca55b61fa513968c64334200214f
parent 72614e079e69aaa3d973b2a885ed99693a74a3d5
Author: Yuukin256 <Yuukin256@gmail.com>
Date: Thu, 10 Nov 2022 01:37:00 +0900
管理画面向け認証機能を実装
Diffstat:
7 files changed, 228 insertions(+), 177 deletions(-)
diff --git a/src/middleware.ts b/src/middleware.ts
@@ -0,0 +1,24 @@
+import { NextResponse } from 'next/server';
+
+import type { NextRequest } from 'next/server';
+
+export const config = {
+ matcher: ['/console/:path*', '/api/rest/:path*'],
+};
+
+export function middleware(req: NextRequest) {
+ const basicAuth = req.headers.get('authorization');
+ const url = req.nextUrl;
+
+ if (basicAuth) {
+ const authValue = basicAuth.split(' ')[1] ?? '';
+ const [user, pwd] = atob(authValue).split(':');
+
+ if (user === process.env['ADMIN_USER'] && pwd === process.env['ADMIN_PASSWORD']) {
+ return NextResponse.next();
+ }
+ }
+
+ url.pathname = '/api/auth';
+ return NextResponse.rewrite(url);
+}
diff --git a/src/pages/[class].tsx b/src/pages/[class].tsx
@@ -1,3 +1,4 @@
+import Box from '@mui/material/Box';
import format from 'date-fns/format';
import { Map } from 'immutable';
@@ -90,7 +91,13 @@ const Page: NextPage<PageProps> = (props) => {
<>
<Head title={`${props.grade}-${props.class}`} />
<Layout title={`${props.grade}-${props.class}`}>
- {Object.keys(props.calendar).length ? <DailyView calendar={Map(props.calendar)} /> : <p>データがありません</p>}
+ {Object.keys(props.calendar).length ? (
+ <DailyView calendar={Map(props.calendar)} />
+ ) : (
+ <Box px={2}>
+ <p>データがありません</p>
+ </Box>
+ )}
</Layout>
</>
);
diff --git a/src/pages/api/auth.ts b/src/pages/api/auth.ts
@@ -0,0 +1,7 @@
+import type { NextApiRequest, NextApiResponse } from 'next';
+
+export default function handler(_: NextApiRequest, res: NextApiResponse) {
+ res.setHeader('WWW-authenticate', 'Basic realm="Secure Area"');
+ res.statusCode = 401;
+ res.end(`Auth Required.`);
+}
diff --git a/src/pages/console/index.tsx b/src/pages/console/index.tsx
@@ -4,30 +4,34 @@ import Breadcrumbs from '@mui/material/Breadcrumbs';
import Container from '@mui/material/Container';
import Typography from '@mui/material/Typography';
+import Head from 'components/common/Head';
import Link from 'components/common/Link';
import type { NextPage } from 'next';
const Page: NextPage = () => {
return (
- <Container maxWidth='lg' component={Box} py={2}>
- <Breadcrumbs separator={<NavigateNextIcon fontSize='small' />} aria-label='breadcrumb'>
- <Link underline='hover' color='inherit' href='/'>
- トップ
- </Link>
- <Typography color='text.primary' variant='h2' fontSize={32} fontWeight={400}>
- 管理
- </Typography>
- </Breadcrumbs>
- <ul>
- <li>
- <Link href={{ pathname: '/console/standard' }}>クラス別時間割表</Link>
- </li>
- <li>
- <Link href={{ pathname: '/console/timetable' }}>学年別曜日時限時間割表</Link>
- </li>
- </ul>
- </Container>
+ <>
+ <Head title='管理' />
+ <Container maxWidth='lg' component={Box} py={2}>
+ <Breadcrumbs separator={<NavigateNextIcon fontSize='small' />} aria-label='breadcrumb'>
+ <Link underline='hover' color='inherit' href='/'>
+ トップ
+ </Link>
+ <Typography color='text.primary' variant='h2' fontSize={32} fontWeight={400}>
+ 管理
+ </Typography>
+ </Breadcrumbs>
+ <ul>
+ <li>
+ <Link href={{ pathname: '/console/standard' }}>クラス別時間割表</Link>
+ </li>
+ <li>
+ <Link href={{ pathname: '/console/timetable' }}>学年別曜日時限時間割表</Link>
+ </li>
+ </ul>
+ </Container>
+ </>
);
};
diff --git a/src/pages/console/standard.tsx b/src/pages/console/standard.tsx
@@ -21,6 +21,7 @@ import TableRow from '@mui/material/TableRow';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
+import Head from 'components/common/Head';
import Link from 'components/common/Link';
import useDebouncedValue from 'hooks/useDebouncedValue';
import useRegisterStandardTimetable from 'hooks/useRegisterStandardTimetable';
@@ -150,52 +151,55 @@ const Page: NextPage = () => {
const debouncedLoading = useDebouncedValue(loading, 1000);
return (
- <Container maxWidth='lg' component={Box} py={2}>
- <Backdrop open={loading && !!debouncedLoading}>
- <CircularProgress color='inherit' />
- </Backdrop>
+ <>
+ <Head title='クラス別時間割表 登録・変更' />
+ <Container maxWidth='lg' component={Box} py={2}>
+ <Backdrop open={loading && !!debouncedLoading}>
+ <CircularProgress color='inherit' />
+ </Backdrop>
- <Breadcrumbs separator={<NavigateNextIcon fontSize='small' />} aria-label='breadcrumb'>
- <Link underline='hover' color='inherit' href='/'>
- トップ
- </Link>
- <Link underline='hover' color='inherit' href='/console'>
- 管理
- </Link>
- <Typography color='text.primary' variant='h2' fontSize={32} fontWeight={400}>
- クラス別時間割表 登録・変更
- </Typography>
- </Breadcrumbs>
+ <Breadcrumbs separator={<NavigateNextIcon fontSize='small' />} aria-label='breadcrumb'>
+ <Link underline='hover' color='inherit' href='/'>
+ トップ
+ </Link>
+ <Link underline='hover' color='inherit' href='/console'>
+ 管理
+ </Link>
+ <Typography color='text.primary' variant='h2' fontSize={32} fontWeight={400}>
+ クラス別時間割表 登録・変更
+ </Typography>
+ </Breadcrumbs>
- <Box width='8rem' m={4}>
- <FormControl fullWidth required disabled={loading} error={typeof classId === 'undefined'}>
- <InputLabel>クラス</InputLabel>
- <Select value={classId ?? ''} label='クラス' onChange={(e) => setClassId(e.target.value as number)}>
- {classes.map((c) => (
- <MenuItem value={c.id} key={c.id}>
- {c.grade}-{c.class}
- </MenuItem>
- ))}
- </Select>
- {typeof classId === 'undefined' && <FormHelperText>選択してください</FormHelperText>}
- </FormControl>
- </Box>
+ <Box width='8rem' m={4}>
+ <FormControl fullWidth required disabled={loading} error={typeof classId === 'undefined'}>
+ <InputLabel>クラス</InputLabel>
+ <Select value={classId ?? ''} label='クラス' onChange={(e) => setClassId(e.target.value as number)}>
+ {classes.map((c) => (
+ <MenuItem value={c.id} key={c.id}>
+ {c.grade}-{c.class}
+ </MenuItem>
+ ))}
+ </Select>
+ {typeof classId === 'undefined' && <FormHelperText>選択してください</FormHelperText>}
+ </FormControl>
+ </Box>
- <StandardTimetableForm standard={standard} setStandard={setStandard} disabled={loading} />
+ <StandardTimetableForm standard={standard} setStandard={setStandard} disabled={loading} />
- <Box m={4}>
- <LoadingButton
- variant='contained'
- loading={sending}
- loadingPosition='start'
- disabled={loading}
- startIcon={<SaveIcon />}
- onClick={() => submit()}
- >
- 保存
- </LoadingButton>
- </Box>
- </Container>
+ <Box m={4}>
+ <LoadingButton
+ variant='contained'
+ loading={sending}
+ loadingPosition='start'
+ disabled={loading}
+ startIcon={<SaveIcon />}
+ onClick={() => submit()}
+ >
+ 保存
+ </LoadingButton>
+ </Box>
+ </Container>
+ </>
);
};
diff --git a/src/pages/console/timetable.tsx b/src/pages/console/timetable.tsx
@@ -24,6 +24,7 @@ import Typography from '@mui/material/Typography';
import isValid from 'date-fns/isValid';
import DatePicker from 'components/common/DatePicker';
+import Head from 'components/common/Head';
import Link from 'components/common/Link';
import {
ABSTRACT_LESSON_NAMES_JA,
@@ -78,131 +79,134 @@ const Page: NextPage = () => {
// TODO: Temporal をそのまま扱える Adapter を用意する?
return (
- <Container maxWidth='lg' component={Box} py={2}>
- <Backdrop open={loading && !!debouncedLoading}>
- <CircularProgress color='inherit' />
- </Backdrop>
+ <>
+ <Head title='曜日時限時間割表 登録・変更' />
+ <Container maxWidth='lg' component={Box} py={2}>
+ <Backdrop open={loading && !!debouncedLoading}>
+ <CircularProgress color='inherit' />
+ </Backdrop>
- <Breadcrumbs separator={<NavigateNextIcon fontSize='small' />} aria-label='breadcrumb'>
- <Link underline='hover' color='inherit' href='/'>
- トップ
- </Link>
- <Link underline='hover' color='inherit' href='/console'>
- 管理
- </Link>
- <Typography color='text.primary' variant='h2' fontSize={32} fontWeight={400}>
- 学年別曜日時限時間割表 登録・変更
- </Typography>
- </Breadcrumbs>
+ <Breadcrumbs separator={<NavigateNextIcon fontSize='small' />} aria-label='breadcrumb'>
+ <Link underline='hover' color='inherit' href='/'>
+ トップ
+ </Link>
+ <Link underline='hover' color='inherit' href='/console'>
+ 管理
+ </Link>
+ <Typography color='text.primary' variant='h2' fontSize={32} fontWeight={400}>
+ 学年別曜日時限時間割表 登録・変更
+ </Typography>
+ </Breadcrumbs>
- <Stack direction={{ xs: 'column', sm: 'row' }} spacing={{ xs: 2, sm: 8 }} m={4}>
- <Box width='8rem'>
- <FormControl fullWidth required disabled={loading} size='small' error={typeof grade === 'undefined'}>
- <InputLabel>学年</InputLabel>
- <Select value={grade ?? ''} label='学年' onChange={(e) => setGrade(e.target.value as number)}>
- {grades.map((g) => (
- <MenuItem value={g} key={g}>
- {g}
- </MenuItem>
- ))}
- </Select>
- {typeof grade === 'undefined' && <FormHelperText>選択してください</FormHelperText>}
- </FormControl>
- </Box>
-
- <Stack direction='row' spacing={1} alignItems='flex-start'>
- <IconButton onClick={() => setDate((old) => old.subtract({ days: 1 }))}>
- <ChevronLeftIcon />
- </IconButton>
- <Box width='12rem'>
- <DatePicker
- value={plainDateToDate(date) ?? null}
- onChange={(newValue) => {
- isValid(newValue) && setDate(dateToPlainDate(newValue as Date));
- }}
- inputFormat='yyyy/MM/dd (EEEEE)'
- disableMaskedInput
- renderInput={(params) => <TextField {...params} />}
- />
+ <Stack direction={{ xs: 'column', sm: 'row' }} spacing={{ xs: 2, sm: 8 }} m={4}>
+ <Box width='8rem'>
+ <FormControl fullWidth required disabled={loading} size='small' error={typeof grade === 'undefined'}>
+ <InputLabel>学年</InputLabel>
+ <Select value={grade ?? ''} label='学年' onChange={(e) => setGrade(e.target.value as number)}>
+ {grades.map((g) => (
+ <MenuItem value={g} key={g}>
+ {g}
+ </MenuItem>
+ ))}
+ </Select>
+ {typeof grade === 'undefined' && <FormHelperText>選択してください</FormHelperText>}
+ </FormControl>
</Box>
- <IconButton onClick={() => setDate((old) => old.add({ days: 1 }))}>
- <ChevronRightIcon />
- </IconButton>
- </Stack>
- </Stack>
- <Paper component={Box} p={4}>
- <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>
- <UnitInputForm
- label='1限'
- value={timetable.period1}
- onChange={(newValue) => setTimetable('period1', newValue)}
- disabled={loading}
- />
- <UnitInputForm
- label='2限'
- value={timetable.period2}
- onChange={(newValue) => setTimetable('period2', newValue)}
- disabled={loading}
- />
- <UnitInputForm
- label='3限'
- value={timetable.period3}
- onChange={(newValue) => setTimetable('period3', newValue)}
- disabled={loading}
- />
- <UnitInputForm
- label='4限'
- value={timetable.period4}
- onChange={(newValue) => setTimetable('period4', newValue)}
- disabled={loading}
- />
- <UnitInputForm
- label='5限'
- value={timetable.period5}
- onChange={(newValue) => setTimetable('period5', newValue)}
- disabled={loading}
- />
- <UnitInputForm
- label='6限'
- value={timetable.period6}
- onChange={(newValue) => setTimetable('period6', newValue)}
- disabled={loading}
- />
- <UnitInputForm
- label='7限'
- value={timetable.period7}
- onChange={(newValue) => setTimetable('period7', newValue)}
- disabled={loading}
- />
+ <Stack direction='row' spacing={1} alignItems='flex-start'>
+ <IconButton onClick={() => setDate((old) => old.subtract({ days: 1 }))}>
+ <ChevronLeftIcon />
+ </IconButton>
+ <Box width='12rem'>
+ <DatePicker
+ value={plainDateToDate(date) ?? null}
+ onChange={(newValue) => {
+ isValid(newValue) && setDate(dateToPlainDate(newValue as Date));
+ }}
+ inputFormat='yyyy/MM/dd (EEEEE)'
+ disableMaskedInput
+ renderInput={(params) => <TextField {...params} />}
+ />
+ </Box>
+ <IconButton onClick={() => setDate((old) => old.add({ days: 1 }))}>
+ <ChevronRightIcon />
+ </IconButton>
+ </Stack>
</Stack>
- <Box mt={2}>
- <Button
- variant='outlined'
- color='secondary'
+ <Paper component={Box} p={4}>
+ <Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>
+ <UnitInputForm
+ label='1限'
+ value={timetable.period1}
+ onChange={(newValue) => setTimetable('period1', newValue)}
+ disabled={loading}
+ />
+ <UnitInputForm
+ label='2限'
+ value={timetable.period2}
+ onChange={(newValue) => setTimetable('period2', newValue)}
+ disabled={loading}
+ />
+ <UnitInputForm
+ label='3限'
+ value={timetable.period3}
+ onChange={(newValue) => setTimetable('period3', newValue)}
+ disabled={loading}
+ />
+ <UnitInputForm
+ label='4限'
+ value={timetable.period4}
+ onChange={(newValue) => setTimetable('period4', newValue)}
+ disabled={loading}
+ />
+ <UnitInputForm
+ label='5限'
+ value={timetable.period5}
+ onChange={(newValue) => setTimetable('period5', newValue)}
+ disabled={loading}
+ />
+ <UnitInputForm
+ label='6限'
+ value={timetable.period6}
+ onChange={(newValue) => setTimetable('period6', newValue)}
+ disabled={loading}
+ />
+ <UnitInputForm
+ label='7限'
+ value={timetable.period7}
+ onChange={(newValue) => setTimetable('period7', newValue)}
+ disabled={loading}
+ />
+ </Stack>
+
+ <Box mt={2}>
+ <Button
+ variant='outlined'
+ color='secondary'
+ disabled={loading}
+ startIcon={<EditIcon />}
+ onClick={() => setUsualTimetable()}
+ >
+ 時間割通りで自動入力
+ </Button>
+ </Box>
+ </Paper>
+
+ <Box m={4}>
+ <LoadingButton
+ variant='contained'
+ loading={sending}
+ loadingPosition='start'
disabled={loading}
- startIcon={<EditIcon />}
- onClick={() => setUsualTimetable()}
+ startIcon={<SaveIcon />}
+ onClick={() => submit()}
>
- 時間割通りで自動入力
- </Button>
+ 保存
+ </LoadingButton>
</Box>
- </Paper>
-
- <Box m={4}>
- <LoadingButton
- variant='contained'
- loading={sending}
- loadingPosition='start'
- disabled={loading}
- startIcon={<SaveIcon />}
- onClick={() => submit()}
- >
- 保存
- </LoadingButton>
- </Box>
- </Container>
+ </Container>
+ </>
);
};
diff --git a/src/prisma/schema.prisma b/src/prisma/schema.prisma
@@ -81,6 +81,7 @@ model DayTimetable {
@@id([grade, date])
}
+// TODO: モデル定期変更 DayTimetable に近い形にする
model UnusualChange {
id Int @id @default(autoincrement())
class SchoolClass @relation(fields: [classId], references: [id])