the tokening

This commit is contained in:
dicedtomatoreal
2020-10-04 21:50:25 -07:00
parent 349c4d0119
commit 91c37007a9
8 changed files with 149 additions and 14 deletions

13
package-lock.json generated
View File

@@ -2571,6 +2571,14 @@
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
},
"copy-to-clipboard": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz",
"integrity": "sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==",
"requires": {
"toggle-selection": "^1.0.6"
}
},
"core-js-compat": {
"version": "3.6.5",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.5.tgz",
@@ -6836,6 +6844,11 @@
"repeat-string": "^1.6.1"
}
},
"toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
},
"toml": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",

View File

@@ -9,6 +9,7 @@
"@material-ui/lab": "^4.0.0-alpha.56",
"bcrypt": "^5.0.0",
"clsx": "^1.1.1",
"copy-to-clipboard": "^3.3.1",
"crypto-js": "^4.0.0",
"fastify": "^3.5.0",
"fastify-cookie": "^4.1.0",

View File

@@ -2,7 +2,7 @@ import { FastifyReply, FastifyRequest, FastifyInstance } from 'fastify';
import { Controller, GET, POST, FastifyInstanceToken, Inject, Hook } from 'fastify-decorators';
import { UserNotFoundError, MissingBodyData, LoginError, UserExistsError, NotAdministratorError } from '../lib/api/APIErrors';
import { User } from '../lib/Data';
import { checkPassword, decrypt, encrypt, encryptPassword } from '../lib/Encryption';
import { checkPassword, createToken, encryptPassword } from '../lib/Encryption';
@Controller('/api/user')
export class UserController {
@@ -41,6 +41,18 @@ export class UserController {
.send(user);
}
@POST('/reset-token')
async resetToken(req: FastifyRequest, reply: FastifyReply) {
if (!req.cookies.zipline) throw new LoginError(`Not logged in.`);
const users = this.instance.mongo.db.collection('zipline_users');
const user: User = await users.findOne({ _id: new this.instance.mongo.ObjectId(req.cookies.zipline) });
if (!user) throw new UserNotFoundError(`User was not found.`);
users.updateOne({ _id: new this.instance.mongo.ObjectId(req.cookies.zipline) }, { $set: { token: createToken() } });
return reply.send({ updated: true });
}
@POST('/create')
async create(req: FastifyRequest<{ Body: { username: string, password: string, administrator: boolean } }>, reply: FastifyReply) {
if (!req.body.username) throw new MissingBodyData(`Missing username.`);
@@ -54,6 +66,7 @@ export class UserController {
const newUser: User = {
username: req.body.username,
password: encryptPassword(req.body.password),
token: createToken(),
administrator: req.body.administrator
};

View File

@@ -9,7 +9,6 @@ import { AddressInfo } from 'net';
import { ConsoleFormatter } from './lib/ConsoleFormatter';
import { bold, green, reset } from '@dicedtomato/colors';
import { Config, Configuration } from './lib/Config';
import { decrypt, encrypt } from './lib/Encryption';
Console.setFormatter(new ConsoleFormatter());

View File

@@ -1,6 +1,7 @@
export interface User {
username: string;
password?: string;
token?: string;
administrator: boolean;
_id?: any;
}

View File

@@ -5,12 +5,8 @@ import { Configuration } from './Config';
const config = Configuration.readConfig();
if (!config) process.exit(0);
export function encrypt(data: any) {
return aes.encrypt(JSON.stringify(data), config.core.secret).toString();
}
export function decrypt(data: string) {
return aes.decrypt(data, config.core.secret).toString();
export function createToken() {
return aes.encrypt(Math.random().toString(36).substr(2) + Date.now(), config.core.secret).toString();
}
export function encryptPassword(pass: string) {

View File

@@ -8,6 +8,14 @@ import TextField from '@material-ui/core/TextField';
import Divider from '@material-ui/core/Divider';
import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import Alert from '@material-ui/lab/Alert';
import Snackbar from '@material-ui/core/Snackbar';
import copy from 'copy-to-clipboard';
import { makeStyles } from '@material-ui/core';
import { store } from '../lib/store';
import { useDispatch } from 'react-redux';
@@ -25,24 +33,102 @@ const useStyles = makeStyles({
export default function IndexPage() {
const classes = useStyles();
const router = useRouter();
const dispatch = useDispatch();
const state = store.getState();
const [alertMessage, setAlertMessage] = useState('Copied token!');
const [tokenOpen, setTokenOpen] = useState(false);
const [resetToken, setResetToken] = useState(false);
const [alertOpen, setAlertOpen] = useState(false);
const handleCopyTokenThenClose = async () => {
const data = await (await fetch('/api/user/current')).json();
if (!data.error) {
copy(data.token);
setAlertMessage('Copied token!');
setTokenOpen(false);
setAlertOpen(true);
}
};
const handleResetTokenThenClose = async () => {
const data = await (await fetch('/api/user/reset-token', { method: 'POST' })).json();
if (!data.error && data.updated) {
setAlertMessage('Reset token!');
setResetToken(false);
setAlertOpen(true);
}
};
if (typeof window !== 'undefined' && !state.loggedIn) router.push('/login');
else {
return (
<UI>
<Snackbar
anchorOrigin={{
vertical: 'top',
horizontal: 'center',
}}
open={alertOpen}
autoHideDuration={6000}
onClose={() => setAlertOpen(false)}
>
<Alert severity="success" variant="filled">
{alertMessage}
</Alert>
</Snackbar>
<Paper elevation={3} className={classes.padding}>
<Typography variant="h5">Welcome back, {state.user.username}</Typography>
<Typography color="textSecondary">You have <b>2</b> images</Typography>
<div className={classes.margin}>
<Typography variant="h5">Token</Typography>
<Button variant="contained" color="primary" className={classes.margin}>
<Button variant="contained" color="primary" className={classes.margin} onClick={() => setTokenOpen(true)}>
Copy
</Button>
<Button variant="contained" className={classes.margin} style={{ backgroundColor: "#d6381c", color: "white" }}>
</Button>
<Dialog
open={tokenOpen}
onClose={() => setTokenOpen(true)}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
This token is used to upload images to Zipline, and should not be shared!
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setTokenOpen(true)} color="primary">
Close
</Button>
<Button onClick={handleCopyTokenThenClose} color="primary" autoFocus>
Yes, copy!
</Button>
</DialogActions>
</Dialog>
<Button variant="contained" className={classes.margin} onClick={() => setResetToken(true)} style={{ backgroundColor: "#d6381c", color: "white" }}>
Reset
</Button>
<Dialog
open={resetToken}
onClose={() => setResetToken(true)}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
This token is used to upload images to Zipline, resetting your token will cause any uploading actions to not work until you update them your self.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setResetToken(true)} color="primary">
Close
</Button>
<Button onClick={handleResetTokenThenClose} color="primary" autoFocus>
Yes, reset!
</Button>
</DialogActions>
</Dialog>
</div>
<Divider />
<div className={classes.margin}>

View File

@@ -5,6 +5,7 @@ import Button from '@material-ui/core/Button';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions';
import Snackbar from '@material-ui/core/Snackbar';
import Grid from '@material-ui/core/Grid';
import { makeStyles } from '@material-ui/core';
import { useDispatch } from 'react-redux';
@@ -12,7 +13,7 @@ import Router from 'next/router';
import { store, persistor } from '../lib/store';
import { UPDATE_USER, LOGIN } from '../lib/reducer';
import UIPlaceholder from '../components/UIPlaceholder';
import Alert from '@material-ui/lab/Alert';
const useStyles = makeStyles({
field: {
width: '100%'
@@ -28,6 +29,16 @@ export default function Index() {
const state = store.getState();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen(true);
};
const handleClose = (event, reason) => {
if (reason === 'clickaway') return;
setOpen(false);
};
const handleLogin = async () => {
const d = await (await fetch('/api/user/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) })).json()
@@ -35,18 +46,33 @@ export default function Index() {
dispatch({ type: UPDATE_USER, payload: d });
dispatch({ type: LOGIN })
Router.push('/');
} else {
setOpen(true);
}
}
if (state.loggedIn) Router.push('/');
else return (
<div>
<Snackbar
anchorOrigin={{
vertical: 'top',
horizontal: 'center',
}}
open={open}
autoHideDuration={6000}
onClose={handleClose}
>
<Alert severity="error" variant="filled">
Could not login!
</Alert>
</Snackbar>
<Grid container spacing={0} direction="column" alignItems="center" justify="center" style={{ minHeight: '100vh' }}>
<Grid item xs={6}>
<Card>
<CardContent>
<Typography color="textSecondary" variant="h3" gutterBottom>
Login
Login
</Typography>
<TextField label="Username" className={classes.field} onChange={(e) => setUsername(e.target.value)} />
<TextField label="Password" className={classes.field} onChange={(e) => setPassword(e.target.value)} />