typeorm & formatting

This commit is contained in:
dicedtomatoreal 2020-10-06 20:01:55 -07:00
parent 21bc4eca48
commit e028afeb3f
28 changed files with 577 additions and 396 deletions

4
.prettierignore Normal file
View file

@ -0,0 +1,4 @@
.next
dist
node_modules
Zipline.toml

1
.prettierrc.json Normal file
View file

@ -0,0 +1 @@
{}

2
next-env.d.ts vendored
View file

@ -1,2 +1,2 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/types/global" /> /// <reference types="next/types/global" />

6
package-lock.json generated
View file

@ -5761,6 +5761,12 @@
"xtend": "^4.0.0" "xtend": "^4.0.0"
} }
}, },
"prettier": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz",
"integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==",
"dev": true
},
"process": { "process": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",

View file

@ -41,6 +41,7 @@
"@types/react-redux": "^7.1.9", "@types/react-redux": "^7.1.9",
"mongodb": "^3.6.2", "mongodb": "^3.6.2",
"pg": "^8.4.0", "pg": "^8.4.0",
"prettier": "2.1.2",
"ts-node": "^9.0.0", "ts-node": "^9.0.0",
"typescript": "^4.0.3" "typescript": "^4.0.3"
} }

View file

@ -1,49 +1,48 @@
import React from 'react'; import React from "react";
import Link from 'next/link'; import Link from "next/link";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import AppBar from '@material-ui/core/AppBar'; import AppBar from "@material-ui/core/AppBar";
import CssBaseline from '@material-ui/core/CssBaseline'; import CssBaseline from "@material-ui/core/CssBaseline";
import Divider from '@material-ui/core/Divider'; import Divider from "@material-ui/core/Divider";
import Drawer from '@material-ui/core/Drawer'; import Drawer from "@material-ui/core/Drawer";
import Hidden from '@material-ui/core/Hidden'; import Hidden from "@material-ui/core/Hidden";
import IconButton from '@material-ui/core/IconButton'; import IconButton from "@material-ui/core/IconButton";
import HomeIcon from '@material-ui/icons/Home'; import HomeIcon from "@material-ui/icons/Home";
import DataUsageIcon from '@material-ui/icons/DataUsage'; import DataUsageIcon from "@material-ui/icons/DataUsage";
import PhotoIcon from '@material-ui/icons/Photo'; import PhotoIcon from "@material-ui/icons/Photo";
import LinkIcon from '@material-ui/icons/Link'; import LinkIcon from "@material-ui/icons/Link";
import List from '@material-ui/core/List'; import List from "@material-ui/core/List";
import ListItem from '@material-ui/core/ListItem'; import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from '@material-ui/core/ListItemText'; import ListItemText from "@material-ui/core/ListItemText";
import MailIcon from '@material-ui/icons/Mail'; import MailIcon from "@material-ui/icons/Mail";
import MenuIcon from '@material-ui/icons/Menu'; import MenuIcon from "@material-ui/icons/Menu";
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from "@material-ui/core/Toolbar";
import Typography from '@material-ui/core/Typography'; import Typography from "@material-ui/core/Typography";
import { makeStyles, useTheme } from '@material-ui/core/styles'; import { makeStyles, useTheme } from "@material-ui/core/styles";
const drawerWidth = 240; const drawerWidth = 240;
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
display: 'flex', display: "flex",
}, },
drawer: { drawer: {
[theme.breakpoints.up('sm')]: { [theme.breakpoints.up("sm")]: {
width: drawerWidth, width: drawerWidth,
flexShrink: 0, flexShrink: 0,
}, },
}, },
appBar: { appBar: {
[theme.breakpoints.up('sm')]: { [theme.breakpoints.up("sm")]: {
width: `calc(100%)`, width: `calc(100%)`,
marginLeft: drawerWidth, marginLeft: drawerWidth,
}, },
}, },
menuButton: { menuButton: {
marginRight: theme.spacing(2), marginRight: theme.spacing(2),
[theme.breakpoints.up('sm')]: { [theme.breakpoints.up("sm")]: {
display: 'none', display: "none",
}, },
}, },
// necessary for content to be below app bar // necessary for content to be below app bar
@ -83,7 +82,7 @@ export default function UI(props) {
</IconButton> </IconButton>
<Typography variant="h6" noWrap> <Typography variant="h6" noWrap>
Zipline Zipline
</Typography> </Typography>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
</Toolbar> </Toolbar>
@ -91,36 +90,44 @@ export default function UI(props) {
<List> <List>
<Link href="/"> <Link href="/">
<ListItem button key="Home"> <ListItem button key="Home">
<ListItemIcon><HomeIcon /></ListItemIcon> <ListItemIcon>
<HomeIcon />
</ListItemIcon>
<ListItemText primary="Home" /> <ListItemText primary="Home" />
</ListItem> </ListItem>
</Link> </Link>
<Link href="/statistics"> <Link href="/statistics">
<ListItem button key="Statistics"> <ListItem button key="Statistics">
<ListItemIcon><DataUsageIcon /></ListItemIcon> <ListItemIcon>
<DataUsageIcon />
</ListItemIcon>
<ListItemText primary="Statistics" /> <ListItemText primary="Statistics" />
</ListItem> </ListItem>
</Link> </Link>
<Link href="/images"> <Link href="/images">
<ListItem button key="Images"> <ListItem button key="Images">
<ListItemIcon><PhotoIcon /></ListItemIcon> <ListItemIcon>
<PhotoIcon />
</ListItemIcon>
<ListItemText primary="Images" /> <ListItemText primary="Images" />
</ListItem> </ListItem>
</Link> </Link>
<Link href="/urls"> <Link href="/urls">
<ListItem button key="URLs"> <ListItem button key="URLs">
<ListItemIcon><LinkIcon /></ListItemIcon> <ListItemIcon>
<LinkIcon />
</ListItemIcon>
<ListItemText primary="URLs" /> <ListItemText primary="URLs" />
</ListItem> </ListItem>
</Link> </Link>
</List> </List>
</div > </div>
); );
const container = window !== undefined ? () => window().document.body : undefined; const container =
window !== undefined ? () => window().document.body : undefined;
return ( return (
<div className={classes.root}> <div className={classes.root}>
<AppBar position="fixed" className={classes.appBar}> <AppBar position="fixed" className={classes.appBar}>
<Toolbar> <Toolbar>
@ -143,7 +150,7 @@ export default function UI(props) {
<Drawer <Drawer
container={container} container={container}
variant="temporary" variant="temporary"
anchor={theme.direction === 'rtl' ? 'right' : 'left'} anchor={theme.direction === "rtl" ? "right" : "left"}
open={mobileOpen} open={mobileOpen}
onClose={handleDrawerToggle} onClose={handleDrawerToggle}
classes={{ classes={{
@ -175,4 +182,4 @@ export default function UI(props) {
</main> </main>
</div> </div>
); );
} }

View file

@ -1,43 +1,42 @@
import React from 'react'; import React from "react";
import Link from 'next/link'; import Link from "next/link";
import AppBar from '@material-ui/core/AppBar'; import AppBar from "@material-ui/core/AppBar";
import Drawer from '@material-ui/core/Drawer'; import Drawer from "@material-ui/core/Drawer";
import Hidden from '@material-ui/core/Hidden'; import Hidden from "@material-ui/core/Hidden";
import IconButton from '@material-ui/core/IconButton'; import IconButton from "@material-ui/core/IconButton";
import List from '@material-ui/core/List'; import List from "@material-ui/core/List";
import ListItem from '@material-ui/core/ListItem'; import ListItem from "@material-ui/core/ListItem";
import Box from '@material-ui/core/Box'; import Box from "@material-ui/core/Box";
import MenuIcon from '@material-ui/icons/Menu'; import MenuIcon from "@material-ui/icons/Menu";
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from "@material-ui/core/Toolbar";
import Typography from '@material-ui/core/Typography'; import Typography from "@material-ui/core/Typography";
import CircularProgress from '@material-ui/core/CircularProgress'; import CircularProgress from "@material-ui/core/CircularProgress";
import Grid from '@material-ui/core/Grid'; import Grid from "@material-ui/core/Grid";
import Skeleton from '@material-ui/lab/Skeleton'; import Skeleton from "@material-ui/lab/Skeleton";
import { makeStyles, useTheme } from '@material-ui/core/styles'; import { makeStyles, useTheme } from "@material-ui/core/styles";
const drawerWidth = 240; const drawerWidth = 240;
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
display: 'flex', display: "flex",
}, },
drawer: { drawer: {
[theme.breakpoints.up('sm')]: { [theme.breakpoints.up("sm")]: {
width: drawerWidth, width: drawerWidth,
flexShrink: 0, flexShrink: 0,
}, },
}, },
appBar: { appBar: {
[theme.breakpoints.up('sm')]: { [theme.breakpoints.up("sm")]: {
width: `calc(100%)`, width: `calc(100%)`,
marginLeft: drawerWidth, marginLeft: drawerWidth,
}, },
}, },
menuButton: { menuButton: {
marginRight: theme.spacing(2), marginRight: theme.spacing(2),
[theme.breakpoints.up('sm')]: { [theme.breakpoints.up("sm")]: {
display: 'none', display: "none",
}, },
}, },
// necessary for content to be below app bar // necessary for content to be below app bar
@ -50,8 +49,8 @@ const useStyles = makeStyles((theme) => ({
padding: theme.spacing(3), padding: theme.spacing(3),
}, },
fullWidth: { fullWidth: {
width: '100%' width: "100%",
} },
})); }));
export default function UIPlaceholder(props) { export default function UIPlaceholder(props) {
@ -80,7 +79,7 @@ export default function UIPlaceholder(props) {
</IconButton> </IconButton>
<Typography variant="h6" noWrap> <Typography variant="h6" noWrap>
Zipline Zipline
</Typography> </Typography>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
</Toolbar> </Toolbar>
@ -99,13 +98,13 @@ export default function UIPlaceholder(props) {
<Skeleton animation="wave" className={classes.fullWidth} /> <Skeleton animation="wave" className={classes.fullWidth} />
</ListItem> </ListItem>
</List> </List>
</div > </div>
); );
const container = window !== undefined ? () => window().document.body : undefined; const container =
window !== undefined ? () => window().document.body : undefined;
return ( return (
<div className={classes.root}> <div className={classes.root}>
<AppBar position="fixed" className={classes.appBar}> <AppBar position="fixed" className={classes.appBar}>
<Toolbar> <Toolbar>
@ -128,7 +127,7 @@ export default function UIPlaceholder(props) {
<Drawer <Drawer
container={container} container={container}
variant="temporary" variant="temporary"
anchor={theme.direction === 'rtl' ? 'right' : 'left'} anchor={theme.direction === "rtl" ? "right" : "left"}
open={mobileOpen} open={mobileOpen}
onClose={handleDrawerToggle} onClose={handleDrawerToggle}
classes={{ classes={{
@ -156,7 +155,14 @@ export default function UIPlaceholder(props) {
<main className={classes.content}> <main className={classes.content}>
<div className={classes.toolbar} /> <div className={classes.toolbar} />
<Grid container spacing={0} direction="column" alignItems="center" justify="center" style={{ minHeight: '80vh' }}> <Grid
container
spacing={0}
direction="column"
alignItems="center"
justify="center"
style={{ minHeight: "80vh" }}
>
<Grid item xs={3}> <Grid item xs={3}>
<CircularProgress size={100} /> <CircularProgress size={100} />
</Grid> </Grid>
@ -164,4 +170,4 @@ export default function UIPlaceholder(props) {
</main> </main>
</div> </div>
); );
} }

View file

@ -1,14 +1,19 @@
import { FastifyReply, FastifyRequest, FastifyInstance } from 'fastify'; import { FastifyReply, FastifyRequest, FastifyInstance } from "fastify";
import { Controller, GET, POST, FastifyInstanceToken, Inject, Hook } from 'fastify-decorators'; import {
import { User } from '../entities/User'; Controller,
GET,
POST,
FastifyInstanceToken,
Inject,
Hook,
} from "fastify-decorators";
import { User } from "../entities/User";
@Controller('/api') @Controller("/api")
export class RootController { export class RootController {
@Inject(FastifyInstanceToken) @Inject(FastifyInstanceToken)
private instance!: FastifyInstance; private instance!: FastifyInstance;
@POST('/upload') @POST("/upload")
async loginStatus(req: FastifyRequest, reply: FastifyReply) { async loginStatus(req: FastifyRequest, reply: FastifyReply) {}
}
}
}

View file

@ -1,105 +1,150 @@
// import { FastifyReply, FastifyRequest, FastifyInstance } from 'fastify'; import { FastifyReply, FastifyRequest, FastifyInstance } from "fastify";
// import { Controller, GET, POST, FastifyInstanceToken, Inject, Hook } from 'fastify-decorators'; import {
// import { UserNotFoundError, MissingBodyData, LoginError, UserExistsError, NotAdministratorError } from '../lib/api/APIErrors'; Controller,
// import { User } from '../lib/Data'; GET,
// import { checkPassword, createToken, encryptPassword } from '../lib/Encryption'; POST,
FastifyInstanceToken,
Inject,
Hook,
} from "fastify-decorators";
import { Repository } from "typeorm";
import { User } from "../entities/User";
import {
UserNotFoundError,
MissingBodyData,
LoginError,
UserExistsError,
NotAdministratorError,
} from "../lib/api/APIErrors";
import {
checkPassword,
createBaseCookie,
createToken,
encryptPassword,
readBaseCookie,
} from "../lib/Encryption";
// @Controller('/api/user') @Controller("/api/user")
// export class UserController { export class UserController {
// @Inject(FastifyInstanceToken) @Inject(FastifyInstanceToken)
// private instance!: FastifyInstance; private instance!: FastifyInstance;
// @GET('/login-status') private users: Repository<User> = this.instance.orm.getRepository(User);
// async loginStatus(req: FastifyRequest, reply: FastifyReply) {
// return reply.send({ user: !!req.cookies.zipline });
// }
// @GET('/current') @GET("/login-status")
// async currentUser(req: FastifyRequest, reply: FastifyReply) { async loginStatus(req: FastifyRequest, reply: FastifyReply) {
// if (!req.cookies.zipline) throw new LoginError(`Not logged in.`); return reply.send({
// const user = await this.instance.mongo.db.collection('zipline_users').findOne({ _id: new this.instance.mongo.ObjectId(req.cookies.zipline) }); user: !!req.cookies.zipline,
// if (!user) throw new UserExistsError(`User doesn't exist`); });
// delete user.password; }
// return reply.send(user);
// }
// @POST('/login') @GET("/current")
// async login(req: FastifyRequest<{ Body: { username: string, password: string } }>, reply: FastifyReply) { async currentUser(req: FastifyRequest, reply: FastifyReply) {
// if (req.cookies.zipline) throw new LoginError(`Already logged in.`) if (!req.cookies.zipline) throw new LoginError(`Not logged in.`);
// if (!req.body.username) throw new MissingBodyData(`Missing username.`); const user = await this.users.findOne({
// if (!req.body.password) throw new MissingBodyData(`Missing uassword.`); where: {
id: readBaseCookie(req.cookies.zipline),
},
});
if (!user) throw new UserExistsError(`User doesn't exist`);
delete user.password;
return reply.send(user);
}
// const user: User = await this.instance.mongo.db.collection('zipline_users').findOne({ @POST("/login")
// username: req.body.username async login(
// }); req: FastifyRequest<{ Body: { username: string; password: string } }>,
reply: FastifyReply
) {
if (req.cookies.zipline) throw new LoginError(`Already logged in.`);
if (!req.body.username) throw new MissingBodyData(`Missing username.`);
if (!req.body.password) throw new MissingBodyData(`Missing uassword.`);
// if (!user) throw new UserNotFoundError(`User "${req.body.username}" was not found.`); const user = await this.users.findOne({
// if (!checkPassword(req.body.password, user.password)) throw new LoginError(`Wrong credentials!`); where: {
// delete user.password; id: readBaseCookie(req.cookies.zipline),
// return reply },
// .setCookie("zipline", user._id, { path: '/' }) });
// .send(user);
// }
// @POST('/logout') if (!user)
// async logout(req: FastifyRequest, reply: FastifyReply) { throw new UserNotFoundError(`User "${req.body.username}" was not found.`);
// if (!req.cookies.zipline) throw new LoginError(`Not logged in.`); if (!checkPassword(req.body.password, user.password))
// try { throw new LoginError(`Wrong credentials!`);
// reply.clearCookie('zipline', { path: '/' }).send({ clearStore: true }) delete user.password;
// } catch (e) {
// reply.send({ clearStore: false });
// }
// }
// @POST('/reset-token') return reply
// async resetToken(req: FastifyRequest, reply: FastifyReply) { .setCookie("zipline", createBaseCookie(user.id), { path: "/" })
// if (!req.cookies.zipline) throw new LoginError(`Not logged in.`); .send(user);
}
// const users = this.instance.mongo.db.collection('zipline_users'); @POST("/logout")
// const user: User = await users.findOne({ _id: new this.instance.mongo.ObjectId(req.cookies.zipline) }); async logout(req: FastifyRequest, reply: FastifyReply) {
// if (!user) throw new UserNotFoundError(`User was not found.`); if (!req.cookies.zipline) throw new LoginError(`Not logged in.`);
try {
reply.clearCookie("zipline", { path: "/" }).send({ clearStore: true });
} catch (e) {
reply.send({ clearStore: false });
}
}
// users.updateOne({ _id: new this.instance.mongo.ObjectId(req.cookies.zipline) }, { $set: { token: createToken() } }); @POST("/reset-token")
// return reply.send({ updated: true }); async resetToken(req: FastifyRequest, reply: FastifyReply) {
// } if (!req.cookies.zipline) throw new LoginError(`Not logged in.`);
// @POST('/create') const user = await this.users.findOne({
// async create(req: FastifyRequest<{ Body: { username: string, password: string, administrator: boolean } }>, reply: FastifyReply) { where: {
// if (!req.body.username) throw new MissingBodyData(`Missing username.`); id: readBaseCookie(req.cookies.zipline),
// if (!req.body.password) throw new MissingBodyData(`Missing uassword.`); },
});
// const users = this.instance.mongo.db.collection('zipline_users'); if (!user) throw new UserNotFoundError(`User was not found.`);
// const existingUser = await users.findOne({ username: req.body.username }); user.token = createToken();
// if (existingUser) throw new UserExistsError('User exists already'); this.users.save(user);
// const newUser: User = { return reply.send({ updated: true });
// username: req.body.username, }
// password: encryptPassword(req.body.password),
// token: createToken(),
// administrator: req.body.administrator
// };
// try { @POST("/create")
// users.insertOne(newUser); async create(
// } catch (e) { req: FastifyRequest<{
// throw new Error(`Could not create user: ${e.message}`); Body: { username: string; password: string; administrator: boolean };
// } }>,
reply: FastifyReply
) {
if (!req.body.username) throw new MissingBodyData(`Missing username.`);
if (!req.body.password) throw new MissingBodyData(`Missing uassword.`);
// return reply.send(newUser); const existing = await this.users.findOne({
// } where: { username: req.body.username },
});
if (existing) throw new UserExistsError("User exists already");
// @Hook('preValidation') try {
// public async preValidation(req: FastifyRequest, reply: FastifyReply) { const user = await this.users.save(
// const adminRoutes = ['/api/user/create']; new User(
req.body.username,
req.body.password,
createToken(),
req.body.administrator || false
)
);
delete user.password;
return reply.send(user);
} catch (e) {
throw new Error(`Could not create user: ${e.message}`);
}
}
// if (adminRoutes.includes(req.routerPath)) { @Hook("preValidation")
// if (!req.cookies.zipline) return reply.send({ error: "You are not logged in" }); public async preValidation(req: FastifyRequest, reply: FastifyReply) {
// const adminRoutes = ['/api/user/create'];
// const admin = await this.instance.mongo.db.collection('zipline_users').findOne({ _id: req.cookies.zipline }); // if (adminRoutes.includes(req.routerPath)) {
// if (!admin) return reply.send({ error: "You are not an administrator" }); // if (!req.cookies.zipline) return reply.send({ error: "You are not logged in" });
// return; // const admin = await this.instance.mongo.db.collection('zipline_users').findOne({ _id: req.cookies.zipline });
// } // if (!admin) return reply.send({ error: "You are not an administrator" });
// return; // return;
// } // }
// } // return;
}
}

View file

@ -2,7 +2,6 @@ import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity() @Entity()
export class User { export class User {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id: number; public id: number;
@ -18,10 +17,15 @@ export class User {
@Column("text") @Column("text")
public token: string; public token: string;
public constructor(username: string, password: string, token: string, administrator: boolean = false) { public constructor(
username: string,
password: string,
token: string,
administrator: boolean = false
) {
this.username = username; this.username = username;
this.password = password; this.password = password;
this.administrator = administrator; this.administrator = administrator;
this.token = token; this.token = token;
} }
} }

View file

@ -1,55 +1,55 @@
import next from 'next'; import next from "next";
import fastify from 'fastify'; import fastify from "fastify";
import fastifyTypeorm from 'fastify-typeorm-plugin'; import fastifyTypeorm from "fastify-typeorm-plugin";
import fastifyCookies from 'fastify-cookie'; import fastifyCookies from "fastify-cookie";
import fastifyMultipart from 'fastify-multipart'; import fastifyMultipart from "fastify-multipart";
import { bootstrap } from 'fastify-decorators'; import { bootstrap } from "fastify-decorators";
import { RootController } from './controllers/RootController'; import { RootController } from "./controllers/RootController";
import { Console } from './lib/logger'; import { Console } from "./lib/logger";
import { AddressInfo } from 'net'; import { AddressInfo } from "net";
import { ConsoleFormatter } from './lib/ConsoleFormatter'; import { ConsoleFormatter } from "./lib/ConsoleFormatter";
import { bold, green, reset } from '@dicedtomato/colors'; import { bold, green, reset } from "@dicedtomato/colors";
import { Configuration } from './lib/Config'; import { Configuration } from "./lib/Config";
// import { UserController } from './controllers/UserController'; // import { UserController } from './controllers/UserController';
Console.setFormatter(new ConsoleFormatter()); Console.setFormatter(new ConsoleFormatter());
const config = Configuration.readConfig(); const config = Configuration.readConfig();
if (!config) process.exit(0); if (!config) process.exit(0);
const server = fastify({}); const server = fastify({});
const dev = process.env.NODE_ENV !== 'production'; const dev = process.env.NODE_ENV !== "production";
const app = next({ dev, quiet: dev }); const app = next({ dev, quiet: dev });
const handle = app.getRequestHandler(); const handle = app.getRequestHandler();
Console.logger("Next").info(`Preparing app`); Console.logger("Next").info(`Preparing app`);
app.prepare(); app.prepare();
if (dev) server.get('/_next/*', (req, reply) => { if (dev)
return handle(req.raw, reply.raw).then(() => reply.sent = true); server.get("/_next/*", (req, reply) => {
}); return handle(req.raw, reply.raw).then(() => (reply.sent = true));
});
server.all('/*', (req, reply) => { server.all("/*", (req, reply) => {
return handle(req.raw, reply.raw).then(() => reply.sent = true); return handle(req.raw, reply.raw).then(() => (reply.sent = true));
}); });
server.setNotFoundHandler((req, reply) => { server.setNotFoundHandler((req, reply) => {
return app.render404(req.raw, reply.raw).then(() => reply.sent = true); return app.render404(req.raw, reply.raw).then(() => (reply.sent = true));
}) });
server.register(fastifyMultipart); server.register(fastifyMultipart);
server.register(fastifyTypeorm, { server.register(fastifyTypeorm, {
...config.database, ...config.database,
entities: [dev ? './src/entities/**/*.ts' : './dist/entities/**/*.js'], entities: [dev ? "./src/entities/**/*.ts" : "./dist/entities/**/*.js"],
synchronize: true, synchronize: true,
logging: false logging: false,
}); });
server.register(bootstrap, { server.register(bootstrap, {
controllers: [ controllers: [
// UserController, // UserController,
RootController RootController,
], ],
}); });
@ -60,11 +60,15 @@ server.register(bootstrap, {
// }); // });
server.register(fastifyCookies, { server.register(fastifyCookies, {
secret: config.core.secret secret: config.core.secret,
}); });
server.listen(config.core.port, err => { server.listen(config.core.port, (err) => {
if (err) throw err; if (err) throw err;
const info = server.server.address() as AddressInfo; const info = server.server.address() as AddressInfo;
Console.logger("Server").info(`server listening on ${bold(`${green(info.address)}${reset(":")}${bold(green(info.port.toString()))}`)}`) Console.logger("Server").info(
}) `server listening on ${bold(
`${green(info.address)}${reset(":")}${bold(green(info.port.toString()))}`
)}`
);
});

View file

@ -1,7 +1,7 @@
import { readFileSync } from 'fs'; import { readFileSync } from "fs";
import { resolve } from 'path'; import { resolve } from "path";
import { parse } from 'toml'; import { parse } from "toml";
import { ConnectionOptions } from 'typeorm'; import { ConnectionOptions } from "typeorm";
export interface Config { export interface Config {
database: ConnectionOptions; database: ConnectionOptions;
@ -21,11 +21,11 @@ export interface ConfigCore {
export class Configuration { export class Configuration {
static readConfig(): Config { static readConfig(): Config {
try { try {
const data = readFileSync(resolve(process.cwd(), "Zipline.toml"), 'utf8'); const data = readFileSync(resolve(process.cwd(), "Zipline.toml"), "utf8");
return parse(data) as Config; return parse(data) as Config;
} catch (e) { } catch (e) {
console.log(e); console.log(e);
return null; return null;
} }
} }
} }

View file

@ -2,7 +2,12 @@ import { bold, magenta, reset } from "@dicedtomato/colors";
import { ConsoleLevel, Formatter } from "./logger"; import { ConsoleLevel, Formatter } from "./logger";
export class ConsoleFormatter implements Formatter { export class ConsoleFormatter implements Formatter {
format(message: string, origin: string, level: ConsoleLevel, time: Date): string { format(
return `${bold(magenta(origin))} ${bold(">")} ${reset(message)}` message: string,
origin: string,
level: ConsoleLevel,
time: Date
): string {
return `${bold(magenta(origin))} ${bold(">")} ${reset(message)}`;
} }
} }

View file

@ -11,4 +11,4 @@ export interface Image {
user: any; user: any;
views: number; views: number;
_id?: any; _id?: any;
} }

View file

@ -1,18 +1,24 @@
import aes from 'crypto-js/aes' import aes from "crypto-js/aes";
import { compareSync, hashSync } from 'bcrypt'; import { compareSync, hashSync } from "bcrypt";
import { Configuration } from './Config'; import { Configuration } from "./Config";
const config = Configuration.readConfig(); const config = Configuration.readConfig();
if (!config) process.exit(0); if (!config) process.exit(0);
export function createRandomId(length: number, charset: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') { export function createRandomId(
let result = ''; length: number,
for (let i = 0; i < length; i++) result += charset.charAt(Math.floor(Math.random() * charset.length)); charset: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
) {
let result = "";
for (let i = 0; i < length; i++)
result += charset.charAt(Math.floor(Math.random() * charset.length));
return result; return result;
} }
export function createToken() { export function createToken() {
return aes.encrypt(`${createRandomId(10)}.${Date.now()}`, config.core.secret).toString(); return aes
.encrypt(`${createRandomId(10)}.${Date.now()}`, config.core.secret)
.toString();
} }
export function encryptPassword(pass: string) { export function encryptPassword(pass: string) {
@ -21,4 +27,12 @@ export function encryptPassword(pass: string) {
export function checkPassword(will: string, equal: string) { export function checkPassword(will: string, equal: string) {
return compareSync(will, equal); return compareSync(will, equal);
} }
export function createBaseCookie(id: number) {
return Buffer.from(id.toString()).toString("base64");
}
export function readBaseCookie(data: string) {
return Buffer.from(data, "base64").toString();
}

View file

@ -1,6 +1,6 @@
export class MissingBodyData extends Error { }; export class MissingBodyData extends Error {}
export class LoginError extends Error { }; export class LoginError extends Error {}
export class NotAdministratorError extends Error { }; export class NotAdministratorError extends Error {}
export class UserExistsError extends Error { }; export class UserExistsError extends Error {}
export class UserNotFoundError extends Error { }; export class UserNotFoundError extends Error {}
export class UserCredentialsError extends Error { }; export class UserCredentialsError extends Error {}

View file

@ -15,10 +15,9 @@ export enum ConsoleLevel {
ERROR, ERROR,
INFO, INFO,
TRACE, TRACE,
WARN WARN,
} }
export class Console { export class Console {
public name: string; public name: string;
@ -59,4 +58,4 @@ export class Console {
const name = o instanceof Function ? o.name : o; const name = o instanceof Function ? o.name : o;
return new Console(name); return new Console(name);
} }
} }

View file

@ -1,13 +1,24 @@
import { ConsoleLevel } from "./Console"; import { ConsoleLevel } from "./Console";
import { brightGreen, blue } from '@dicedtomato/colors'; import { brightGreen, blue } from "@dicedtomato/colors";
export interface Formatter { export interface Formatter {
format(message: string, origin: string, level: ConsoleLevel, time: Date): string; format(
message: string,
origin: string,
level: ConsoleLevel,
time: Date
): string;
} }
export class DefaultFormatter implements Formatter { export class DefaultFormatter implements Formatter {
public format(message: string, origin: string, level: ConsoleLevel, time: Date) { public format(
return `[${time.toLocaleString()}] ${brightGreen(origin)} - ${blue(ConsoleLevel[level])}: ${message}` message: string,
origin: string,
level: ConsoleLevel,
time: Date
) {
return `[${time.toLocaleString()}] ${brightGreen(origin)} - ${blue(
ConsoleLevel[level]
)}: ${message}`;
} }
} }

View file

@ -1,2 +1,2 @@
export * from './Console'; export * from "./Console";
export * from './Formatter'; export * from "./Formatter";

View file

@ -6,13 +6,12 @@ export const LOGOUT = "LOGOUT";
export const UPDATE_USER = "UPDATE_USER"; export const UPDATE_USER = "UPDATE_USER";
export interface State { export interface State {
loggedIn: boolean; loggedIn: boolean;
user: User user: User;
} }
const initialState: State = { const initialState: State = {
loggedIn: false, loggedIn: false,
user: null user: null,
}; };
export function reducer(state: State = initialState, action) { export function reducer(state: State = initialState, action) {
@ -26,4 +25,4 @@ export function reducer(state: State = initialState, action) {
default: default:
return state; return state;
} }
} }

View file

@ -1,15 +1,18 @@
import { createStore } from 'redux'; import { createStore } from "redux";
import { persistStore, persistReducer } from 'redux-persist'; import { persistStore, persistReducer } from "redux-persist";
import storage from 'redux-persist/lib/storage'; import storage from "redux-persist/lib/storage";
import { reducer } from './reducer' import { reducer } from "./reducer";
const persistedReducer = persistReducer({ const persistedReducer = persistReducer(
key: 'root', {
storage, key: "root",
}, reducer); storage,
},
reducer
);
let store = createStore(persistedReducer) let store = createStore(persistedReducer);
let persistor = persistStore(store) let persistor = persistStore(store);
export { store, persistor }; export { store, persistor };

View file

@ -1,21 +1,21 @@
import { createMuiTheme } from '@material-ui/core/styles'; import { createMuiTheme } from "@material-ui/core/styles";
import { red } from '@material-ui/core/colors'; import { red } from "@material-ui/core/colors";
// Create a theme instance. // Create a theme instance.
const theme = createMuiTheme({ const theme = createMuiTheme({
palette: { palette: {
type: 'dark', type: "dark",
primary: { primary: {
main: '#556cd6', main: "#556cd6",
}, },
secondary: { secondary: {
main: '#19857b', main: "#19857b",
}, },
error: { error: {
main: red.A100, main: red.A100,
}, },
background: { background: {
default: '#121212', default: "#121212",
}, },
}, },
}); });

View file

@ -1,18 +1,18 @@
import React, { useEffect } from 'react'; import React, { useEffect } from "react";
import PropTypes from 'prop-types'; import PropTypes from "prop-types";
import Head from 'next/head'; import Head from "next/head";
import { ThemeProvider } from '@material-ui/core/styles'; import { ThemeProvider } from "@material-ui/core/styles";
import CssBaseline from '@material-ui/core/CssBaseline'; import CssBaseline from "@material-ui/core/CssBaseline";
import { Provider } from 'react-redux'; import { Provider } from "react-redux";
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from "redux-persist/integration/react";
import theme from '../lib/theme'; import theme from "../lib/theme";
import { store, persistor } from '../lib/store'; import { store, persistor } from "../lib/store";
import UIPlaceholder from '../components/UIPlaceholder'; import UIPlaceholder from "../components/UIPlaceholder";
export default function MyApp(props) { export default function MyApp(props) {
const { Component, pageProps } = props; const { Component, pageProps } = props;
useEffect(() => { useEffect(() => {
const jssStyles = document.querySelector('#jss-server-side'); const jssStyles = document.querySelector("#jss-server-side");
if (jssStyles) { if (jssStyles) {
jssStyles.parentElement.removeChild(jssStyles); jssStyles.parentElement.removeChild(jssStyles);
} }
@ -24,7 +24,10 @@ export default function MyApp(props) {
<PersistGate loading={<UIPlaceholder />} persistor={persistor}> <PersistGate loading={<UIPlaceholder />} persistor={persistor}>
<Head> <Head>
<title>Zipline</title> <title>Zipline</title>
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" /> <meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
</Head> </Head>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from "react";
import Document, { Html, Head, Main, NextScript } from 'next/document'; import Document, { Html, Head, Main, NextScript } from "next/document";
import { ServerStyleSheets } from '@material-ui/core/styles'; import { ServerStyleSheets } from "@material-ui/core/styles";
import theme from '../lib/theme'; import theme from "../lib/theme";
export default class MyDocument extends Document { export default class MyDocument extends Document {
render() { render() {
@ -63,6 +63,9 @@ MyDocument.getInitialProps = async (ctx) => {
return { return {
...initialProps, ...initialProps,
// Styles fragment is rendered after the app and page rendering finish. // Styles fragment is rendered after the app and page rendering finish.
styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()], styles: [
...React.Children.toArray(initialProps.styles),
sheets.getStyleElement(),
],
}; };
}; };

View file

@ -1,34 +1,33 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import { useRouter } from 'next/router'; import { useRouter } from "next/router";
import UI from '../components/UI'; import UI from "../components/UI";
import UIPlaceholder from '../components/UIPlaceholder'; import UIPlaceholder from "../components/UIPlaceholder";
import Typography from '@material-ui/core/Typography'; import Typography from "@material-ui/core/Typography";
import Paper from '@material-ui/core/Paper'; import Paper from "@material-ui/core/Paper";
import TextField from '@material-ui/core/TextField'; import TextField from "@material-ui/core/TextField";
import Divider from '@material-ui/core/Divider'; import Divider from "@material-ui/core/Divider";
import Button from '@material-ui/core/Button'; import Button from "@material-ui/core/Button";
import Grid from '@material-ui/core/Grid'; import Grid from "@material-ui/core/Grid";
import Dialog from '@material-ui/core/Dialog'; import Dialog from "@material-ui/core/Dialog";
import DialogActions from '@material-ui/core/DialogActions'; import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from '@material-ui/core/DialogContent'; import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from '@material-ui/core/DialogContentText'; import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from '@material-ui/core/DialogTitle'; import DialogTitle from "@material-ui/core/DialogTitle";
import Alert from '@material-ui/lab/Alert'; import Alert from "@material-ui/lab/Alert";
import Snackbar from '@material-ui/core/Snackbar'; import Snackbar from "@material-ui/core/Snackbar";
import copy from 'copy-to-clipboard'; import copy from "copy-to-clipboard";
import { LOGOUT, UPDATE_USER } from '../lib/reducer'; import { LOGOUT, UPDATE_USER } from "../lib/reducer";
import { makeStyles } from '@material-ui/core'; import { makeStyles } from "@material-ui/core";
import { store } from '../lib/store'; import { store } from "../lib/store";
import { useDispatch } from 'react-redux'; import { useDispatch } from "react-redux";
const useStyles = makeStyles({ const useStyles = makeStyles({
margin: { margin: {
margin: '5px' margin: "5px",
}, },
padding: { padding: {
padding: '10px' padding: "10px",
} },
}); });
export default function IndexPage() { export default function IndexPage() {
@ -36,49 +35,53 @@ export default function IndexPage() {
const router = useRouter(); const router = useRouter();
const dispatch = useDispatch(); const dispatch = useDispatch();
const state = store.getState(); const state = store.getState();
const [alertMessage, setAlertMessage] = useState('Copied token!'); const [alertMessage, setAlertMessage] = useState("Copied token!");
const [tokenOpen, setTokenOpen] = useState(false); const [tokenOpen, setTokenOpen] = useState(false);
const [resetToken, setResetToken] = useState(false); const [resetToken, setResetToken] = useState(false);
const [alertOpen, setAlertOpen] = useState(false); const [alertOpen, setAlertOpen] = useState(false);
const handleCopyTokenThenClose = async () => { const handleCopyTokenThenClose = async () => {
const data = await (await fetch('/api/user/current')).json(); const data = await (await fetch("/api/user/current")).json();
if (!data.error) { if (!data.error) {
copy(data.token); copy(data.token);
setAlertMessage('Copied token!'); setAlertMessage("Copied token!");
setTokenOpen(false); setTokenOpen(false);
setAlertOpen(true); setAlertOpen(true);
} }
}; };
const handleResetTokenThenClose = async () => { const handleResetTokenThenClose = async () => {
const data = await (await fetch('/api/user/reset-token', { method: 'POST' })).json(); const data = await (
await fetch("/api/user/reset-token", { method: "POST" })
).json();
if (!data.error && data.updated) { if (!data.error && data.updated) {
setAlertMessage('Reset token!'); setAlertMessage("Reset token!");
setResetToken(false); setResetToken(false);
setAlertOpen(true); setAlertOpen(true);
} }
}; };
const handleLogout = async () => { const handleLogout = async () => {
const data = await (await fetch('/api/user/logout', { method: 'POST' })).json(); const data = await (
await fetch("/api/user/logout", { method: "POST" })
).json();
if (!data.error && data.clearStore) { if (!data.error && data.clearStore) {
dispatch({ type: LOGOUT }); dispatch({ type: LOGOUT });
dispatch({ type: UPDATE_USER, payload: null }); dispatch({ type: UPDATE_USER, payload: null });
setAlertMessage('Logged out!'); setAlertMessage("Logged out!");
setAlertOpen(true); setAlertOpen(true);
router.push('/login'); router.push("/login");
} }
}; };
if (typeof window !== 'undefined' && !state.loggedIn) router.push('/login'); if (typeof window !== "undefined" && !state.loggedIn) router.push("/login");
else { else {
return ( return (
<UI> <UI>
<Snackbar <Snackbar
anchorOrigin={{ anchorOrigin={{
vertical: 'top', vertical: "top",
horizontal: 'center', horizontal: "center",
}} }}
open={alertOpen} open={alertOpen}
autoHideDuration={6000} autoHideDuration={6000}
@ -89,11 +92,20 @@ export default function IndexPage() {
</Alert> </Alert>
</Snackbar> </Snackbar>
<Paper elevation={3} className={classes.padding}> <Paper elevation={3} className={classes.padding}>
<Typography variant="h5">Welcome back, {state.user.username}</Typography> <Typography variant="h5">
<Typography color="textSecondary">You have <b>2</b> images</Typography> Welcome back, {state.user.username}
</Typography>
<Typography color="textSecondary">
You have <b>2</b> images
</Typography>
<div className={classes.margin}> <div className={classes.margin}>
<Typography variant="h5">Token</Typography> <Typography variant="h5">Token</Typography>
<Button variant="contained" color="primary" className={classes.margin} onClick={() => setTokenOpen(true)}> <Button
variant="contained"
color="primary"
className={classes.margin}
onClick={() => setTokenOpen(true)}
>
Copy Copy
</Button> </Button>
<Dialog <Dialog
@ -105,19 +117,29 @@ export default function IndexPage() {
<DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle> <DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText id="alert-dialog-description"> <DialogContentText id="alert-dialog-description">
This token is used to upload images to Zipline, and should not be shared! This token is used to upload images to Zipline, and should not
be shared!
</DialogContentText> </DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setTokenOpen(true)} color="primary"> <Button onClick={() => setTokenOpen(true)} color="primary">
Close Close
</Button> </Button>
<Button onClick={handleCopyTokenThenClose} color="primary" autoFocus> <Button
onClick={handleCopyTokenThenClose}
color="primary"
autoFocus
>
Yes, copy! Yes, copy!
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<Button variant="contained" className={classes.margin} onClick={() => setResetToken(true)} style={{ backgroundColor: "#d6381c", color: "white" }}> <Button
variant="contained"
className={classes.margin}
onClick={() => setResetToken(true)}
style={{ backgroundColor: "#d6381c", color: "white" }}
>
Reset Reset
</Button> </Button>
<Dialog <Dialog
@ -129,14 +151,20 @@ export default function IndexPage() {
<DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle> <DialogTitle id="alert-dialog-title">Are you sure?</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText id="alert-dialog-description"> <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. 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> </DialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={() => setResetToken(true)} color="primary"> <Button onClick={() => setResetToken(true)} color="primary">
Close Close
</Button> </Button>
<Button onClick={handleResetTokenThenClose} color="primary" autoFocus> <Button
onClick={handleResetTokenThenClose}
color="primary"
autoFocus
>
Yes, reset! Yes, reset!
</Button> </Button>
</DialogActions> </DialogActions>
@ -146,23 +174,36 @@ export default function IndexPage() {
<div className={classes.margin}> <div className={classes.margin}>
<Typography variant="h5">User</Typography> <Typography variant="h5">User</Typography>
<TextField label="Username" className={classes.margin} fullWidth /> <TextField label="Username" className={classes.margin} fullWidth />
<TextField label="Password" type="password" className={classes.margin} fullWidth /> <TextField
label="Password"
type="password"
className={classes.margin}
fullWidth
/>
</div> </div>
<Divider /> <Divider />
<div className={classes.margin}> <div className={classes.margin}>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={6}> <Grid item xs={6}>
<Button variant="contained" color="primary" fullWidth>Update</Button> <Button variant="contained" color="primary" fullWidth>
Update
</Button>
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
<Button variant="contained" style={{ backgroundColor: "#d6381c", color: "white" }} fullWidth onClick={handleLogout}>Logout</Button> <Button
variant="contained"
style={{ backgroundColor: "#d6381c", color: "white" }}
fullWidth
onClick={handleLogout}
>
Logout
</Button>
</Grid> </Grid>
</Grid> </Grid>
</div> </div>
</Paper> </Paper>
</UI> </UI>
); );
} }
return <UIPlaceholder />; return <UIPlaceholder />;
} }

View file

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

View file

@ -1,5 +1,5 @@
import UI from '../components/UI'; import UI from "../components/UI";
import { Typography } from '@material-ui/core'; import { Typography } from "@material-ui/core";
export default function MyApp(props) { export default function MyApp(props) {
return ( return (
@ -8,5 +8,5 @@ export default function MyApp(props) {
<Typography>stater</Typography> <Typography>stater</Typography>
</UI> </UI>
</div> </div>
) );
} }

View file

@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "ES2020",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"outDir": "./dist", "outDir": "./dist",
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
@ -20,12 +16,6 @@
"experimentalDecorators": true, "experimentalDecorators": true,
"noEmit": false "noEmit": false
}, },
"include": [ "include": ["next-env.d.ts", "src"],
"next-env.d.ts", "exclude": ["node_modules", ".next"]
"src" }
],
"exclude": [
"node_modules",
".next"
]
}