timetable-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit ac49a787e567ca55b61fa513968c64334200214f
parent 72614e079e69aaa3d973b2a885ed99693a74a3d5
Author: Yuukin256 <Yuukin256@gmail.com>
Date:   Thu, 10 Nov 2022 01:37:00 +0900

管理画面向け認証機能を実装

Diffstat:
Asrc/middleware.ts | 24++++++++++++++++++++++++
Msrc/pages/[class].tsx | 9++++++++-
Asrc/pages/api/auth.ts | 7+++++++
Msrc/pages/console/index.tsx | 40++++++++++++++++++++++------------------
Msrc/pages/console/standard.tsx | 88+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/pages/console/timetable.tsx | 236++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/prisma/schema.prisma | 1+
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])