first
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -30,3 +30,5 @@ yarn-error.log*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
/db.sqlite
|
||||
|
||||
82
libs/db.js
Normal file
82
libs/db.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
const db = new Database("db.sqlite");
|
||||
|
||||
// init DB
|
||||
db.prepare(
|
||||
`CREATE TABLE IF NOT EXISTS time_ranges (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
range TEXT NOT NULL DEFAULT '',
|
||||
username TEXT NOT NULL DEFAULT ''
|
||||
)`
|
||||
).run();
|
||||
db.prepare(
|
||||
`CREATE TABLE IF NOT EXISTS configs (
|
||||
name TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)`
|
||||
).run();
|
||||
db.prepare(
|
||||
`INSERT OR IGNORE INTO configs (name, value) VALUES ('limit', '1')`
|
||||
).run();
|
||||
db.prepare(
|
||||
`INSERT OR IGNORE INTO configs (name, value) VALUES ('token', 'woshimima')`
|
||||
).run();
|
||||
|
||||
// prepare statements
|
||||
const insertTimeRange = db.prepare(
|
||||
`INSERT INTO time_ranges (name, range) VALUES (?, ?)`
|
||||
);
|
||||
const getTimeRanges = db.prepare(`SELECT * FROM time_ranges`);
|
||||
const deleteTimeRange = db.prepare(`DELETE FROM time_ranges WHERE id = ?`);
|
||||
const updateUsername = db.prepare(
|
||||
`UPDATE time_ranges SET username = ? WHERE id = ?`
|
||||
);
|
||||
const countUser = db.prepare(
|
||||
`SELECT COUNT(*) as count FROM time_ranges WHERE username = ?`
|
||||
);
|
||||
const getUsername = db.prepare(`SELECT username FROM time_ranges WHERE id = ?`);
|
||||
const updateUsernameWithLimit = db.transaction((username, id, limit) => {
|
||||
const count = countUser.get(username).count;
|
||||
const existingUsername = getUsername.get(id).username;
|
||||
if (existingUsername !== "") {
|
||||
throw new Error("Username already exists");
|
||||
}
|
||||
if (count >= limit) {
|
||||
throw new Error("Limit reached");
|
||||
}
|
||||
updateUsername.run(username, id);
|
||||
});
|
||||
|
||||
const getConfigStmt = db.prepare(`SELECT value FROM configs WHERE name = ?`);
|
||||
const setConfigStmt = db.prepare(
|
||||
`UPDATE configs SET value = ? WHERE name = ?`
|
||||
);
|
||||
const getLimit = () => {
|
||||
const limit = getConfigStmt.get("limit").value;
|
||||
return parseInt(limit);
|
||||
};
|
||||
const setLimit = (limit) => {
|
||||
setConfigStmt.run(limit, "limit");
|
||||
};
|
||||
const getToken = () => {
|
||||
const token = getConfigStmt.get("token").value;
|
||||
return token;
|
||||
};
|
||||
|
||||
const authenticate = (token) => {
|
||||
const tokenFromDB = getToken();
|
||||
return token === tokenFromDB;
|
||||
};
|
||||
|
||||
export {
|
||||
insertTimeRange,
|
||||
getTimeRanges,
|
||||
deleteTimeRange,
|
||||
updateUsername,
|
||||
updateUsernameWithLimit,
|
||||
getLimit,
|
||||
setLimit,
|
||||
authenticate,
|
||||
};
|
||||
1659
package-lock.json
generated
1659
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mui/icons-material": "^5.5.1",
|
||||
"@mui/material": "^5.5.3",
|
||||
"better-sqlite3": "^7.5.0",
|
||||
"next": "12.1.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
|
||||
@@ -1,7 +1,63 @@
|
||||
import '../styles/globals.css'
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Stack,
|
||||
Box,
|
||||
CssBaseline,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import "../styles/globals.css";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return <Component {...pageProps} />
|
||||
const [username, setUsername] = useState("");
|
||||
pageProps = { ...pageProps, username, setUsername };
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>ITSC Tool</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width"
|
||||
/>
|
||||
<CssBaseline />
|
||||
</Head>
|
||||
<AppBar
|
||||
position="static"
|
||||
sx={{
|
||||
mb: 3,
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
sx={{
|
||||
flex: 1,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">
|
||||
<Link href="/">ITSC Tool</Link>
|
||||
</Typography>
|
||||
{username && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
//localStorage.removeItem("username");
|
||||
setUsername("");
|
||||
}}
|
||||
>
|
||||
{username} (Logout)
|
||||
</Button>
|
||||
)}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp
|
||||
export default MyApp;
|
||||
|
||||
30
pages/api/time/limit.js
Normal file
30
pages/api/time/limit.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { authenticate, setLimit } from "../../../libs/db";
|
||||
|
||||
export default function handler(req, res) {
|
||||
// put method
|
||||
if (req.method === "PUT") {
|
||||
const { token, limit } = req.body;
|
||||
|
||||
// authenticate
|
||||
if (!authenticate(token)) {
|
||||
res.status(401).json({
|
||||
error: "Unauthorized",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// check type is integer
|
||||
const limitInt = parseInt(limit);
|
||||
if (!limitInt) {
|
||||
res.status(400).json({
|
||||
error: "limit must be integer",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLimit(limitInt);
|
||||
res.status(200).send({ success: true });
|
||||
} else {
|
||||
res.status(405).send({ error: "method not allowed" });
|
||||
}
|
||||
}
|
||||
29
pages/api/time/ranges.js
Normal file
29
pages/api/time/ranges.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { authenticate, getTimeRanges, insertTimeRange } from "../../../libs/db";
|
||||
|
||||
export default function handler(req, res) {
|
||||
// get method
|
||||
if (req.method === "GET") {
|
||||
res.setHeader("Cache-Control", "no-cache no-store must-revalidate");
|
||||
res.status(200).json(getTimeRanges.all());
|
||||
return;
|
||||
} else if (req.method === "POST") {
|
||||
// authenticate
|
||||
const { token } = req.body;
|
||||
if (!authenticate(token)) {
|
||||
res.status(401).json({ error: "Unauthorized" });
|
||||
return;
|
||||
}
|
||||
// jsonfiy
|
||||
const { name, range } = req.body;
|
||||
insertTimeRange.run(name, range);
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
// 500 error
|
||||
res.status(500).json({
|
||||
error: "Method not allowed",
|
||||
});
|
||||
}
|
||||
}
|
||||
59
pages/api/time/ranges/[id].js
Normal file
59
pages/api/time/ranges/[id].js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { deleteTimeRange, getLimit, authenticate, updateUsernameWithLimit } from "../../../../libs/db";
|
||||
|
||||
export default function handler(req, res) {
|
||||
// check if id is valid
|
||||
const { id } = req.query;
|
||||
if (id === undefined) {
|
||||
res.status(400).json({
|
||||
error: "Missing id",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// delete method
|
||||
if (req.method === "DELETE") {
|
||||
// authenticate
|
||||
const { token } = req.body;
|
||||
if (!authenticate(token)) {
|
||||
console.log("[DELETE] Authentication failed");
|
||||
res.status(401).json({
|
||||
error: "Unauthenticated",
|
||||
});
|
||||
return;
|
||||
}
|
||||
deleteTimeRange.run(id);
|
||||
|
||||
// update username
|
||||
} else if (req.method === "PUT") {
|
||||
// check if id is valid
|
||||
// check if username is valid
|
||||
const { username } = req.body;
|
||||
if (username === undefined) {
|
||||
res.status(400).json({
|
||||
error: "Missing username",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const limit = getLimit();
|
||||
updateUsernameWithLimit(username, id, limit);
|
||||
} catch (err) {
|
||||
res.status(400).json({
|
||||
error: err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// not allow
|
||||
} else {
|
||||
// 500 error
|
||||
res.status(500).json({
|
||||
error: "Method not allowed",
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
143
pages/index.js
143
pages/index.js
@@ -1,69 +1,82 @@
|
||||
import Head from 'next/head'
|
||||
import Image from 'next/image'
|
||||
import styles from '../styles/Home.module.css'
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
TextField,
|
||||
Stack,
|
||||
InputField,
|
||||
Box,
|
||||
Snackbar,
|
||||
Container,
|
||||
} from "@mui/material";
|
||||
|
||||
export default function Index(props) {
|
||||
const [username, setUsername] = useState("");
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
|
||||
// get username from localStorage
|
||||
useEffect(() => {
|
||||
const localUsername = localStorage.getItem("username");
|
||||
if (localUsername) {
|
||||
setUsername(localUsername);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const login = () => {
|
||||
if (!username) {
|
||||
setSnackbarOpen(true);
|
||||
return;
|
||||
}
|
||||
// set local storage
|
||||
localStorage.setItem("username", username);
|
||||
props.setUsername(username);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.username) {
|
||||
router.push("/time");
|
||||
}
|
||||
}, [props.username]);
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<meta name="description" content="Generated by create next app" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<main className={styles.main}>
|
||||
<h1 className={styles.title}>
|
||||
Welcome to <a href="https://nextjs.org">Next.js!</a>
|
||||
</h1>
|
||||
|
||||
<p className={styles.description}>
|
||||
Get started by editing{' '}
|
||||
<code className={styles.code}>pages/index.js</code>
|
||||
</p>
|
||||
|
||||
<div className={styles.grid}>
|
||||
<a href="https://nextjs.org/docs" className={styles.card}>
|
||||
<h2>Documentation →</h2>
|
||||
<p>Find in-depth information about Next.js features and API.</p>
|
||||
</a>
|
||||
|
||||
<a href="https://nextjs.org/learn" className={styles.card}>
|
||||
<h2>Learn →</h2>
|
||||
<p>Learn about Next.js in an interactive course with quizzes!</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://github.com/vercel/next.js/tree/canary/examples"
|
||||
className={styles.card}
|
||||
<Container>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<TextField
|
||||
label="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
login();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Link href="/time" passHref>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
login();
|
||||
}}
|
||||
>
|
||||
<h2>Examples →</h2>
|
||||
<p>Discover and deploy boilerplate example Next.js projects.</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
className={styles.card}
|
||||
>
|
||||
<h2>Deploy →</h2>
|
||||
<p>
|
||||
Instantly deploy your Next.js site to a public URL with Vercel.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className={styles.footer}>
|
||||
<a
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Powered by{' '}
|
||||
<span className={styles.logo}>
|
||||
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
|
||||
</span>
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
Login
|
||||
</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
<Snackbar
|
||||
open={snackbarOpen}
|
||||
autoHideDuration={1000}
|
||||
onClose={() => setSnackbarOpen(false)}
|
||||
>
|
||||
<Alert variant="filled" severity="error">
|
||||
Username can{"'"}t be empty
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
297
pages/time.js
Normal file
297
pages/time.js
Normal file
@@ -0,0 +1,297 @@
|
||||
import {
|
||||
Container,
|
||||
Box,
|
||||
Alert,
|
||||
Snackbar,
|
||||
Button,
|
||||
TextField,
|
||||
TableContainer,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
} from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export default function Time(props) {
|
||||
const [ranges, setRanges] = useState([]);
|
||||
const [range, setRange] = useState("");
|
||||
const [newName, setNewName] = useState("");
|
||||
const [snackbarError, setSnackbarError] = useState(false);
|
||||
const [snackbarErrorMessage, setSnackbarErrorMessage] = useState("");
|
||||
const [snackbarSuccess, setSnackbarSuccess] = useState(false);
|
||||
const [limit, setLimit] = useState(1);
|
||||
const [token, setToken] = useState("");
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const isAdmin = () => {
|
||||
if (props.username === "admin") {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const addRange = () => {
|
||||
fetch("/api/time/ranges", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: newName,
|
||||
range,
|
||||
token,
|
||||
}),
|
||||
}).then((res) =>
|
||||
res.json().then((res) => {
|
||||
if (res.error) {
|
||||
setSnackbarError(true);
|
||||
setSnackbarErrorMessage(res.error);
|
||||
} else {
|
||||
setSnackbarSuccess(true);
|
||||
refreshRanges();
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const refreshRanges = () => {
|
||||
fetch("/api/time/ranges")
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
setSnackbarError(true);
|
||||
setSnackbarErrorMessage(res.error);
|
||||
} else {
|
||||
setSnackbarSuccess(true);
|
||||
setRanges(res);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteRange = (id) => {
|
||||
fetch(`/api/time/ranges/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ token }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
setSnackbarError(true);
|
||||
setSnackbarErrorMessage(res.error);
|
||||
} else {
|
||||
setSnackbarSuccess(true);
|
||||
refreshRanges();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateUsername = (id, username) => {
|
||||
fetch(`/api/time/ranges/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ username }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
setSnackbarError(true);
|
||||
setSnackbarErrorMessage(res.error);
|
||||
} else {
|
||||
setSnackbarSuccess(true);
|
||||
refreshRanges();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateLimit = (limit) => {
|
||||
fetch("/api/time/limit", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ limit, token }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
setSnackbarError(true);
|
||||
setSnackbarErrorMessage(res.error);
|
||||
} else {
|
||||
setSnackbarSuccess(true);
|
||||
refreshRanges();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.username) {
|
||||
router.push("/");
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
refreshRanges();
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
*/
|
||||
|
||||
useEffect(() => {
|
||||
refreshRanges();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{isAdmin() && (
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
my: 2,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Token"
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
my: 2,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Name"
|
||||
value={newName}
|
||||
onChange={(e) => setNewName(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label="Range"
|
||||
value={range}
|
||||
onChange={(e) => setRange(e.target.value)}
|
||||
placeholder="2022-01-01 00:00:00"
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => addRange()}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Box
|
||||
sx={{
|
||||
my: 2,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Limit"
|
||||
value={limit}
|
||||
onChange={(e) => setLimit(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
updateLimit(limit);
|
||||
}}
|
||||
>
|
||||
Update Limit
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => refreshRanges()}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Range</TableCell>
|
||||
<TableCell>Taken</TableCell>
|
||||
<TableCell>Action</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{ranges.map((range) => (
|
||||
<TableRow key={range.id}>
|
||||
<TableCell>{range.name}</TableCell>
|
||||
<TableCell>{range.range}</TableCell>
|
||||
<TableCell>{range.username}</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
sx={{
|
||||
userSelect: "none",
|
||||
}}
|
||||
disabled={range.username !== ""}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => updateUsername(range.id, props.username)}
|
||||
>
|
||||
Take
|
||||
</Button>
|
||||
{isAdmin() && (
|
||||
<Button
|
||||
sx={{
|
||||
userSelect: "none",
|
||||
}}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => deleteRange(range.id)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Snackbar
|
||||
open={snackbarError}
|
||||
autoHideDuration={1000}
|
||||
onClose={() => setSnackbarError(false)}
|
||||
>
|
||||
<Alert
|
||||
variant="filled"
|
||||
onClose={() => setSnackbarError(false)}
|
||||
severity="error"
|
||||
>
|
||||
{snackbarErrorMessage}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<Snackbar
|
||||
open={snackbarSuccess}
|
||||
autoHideDuration={1000}
|
||||
onClose={() => setSnackbarSuccess(false)}
|
||||
>
|
||||
<Alert
|
||||
variant="filled"
|
||||
onClose={() => setSnackbarSuccess(false)}
|
||||
severity="success"
|
||||
>
|
||||
Success!
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user