Compare commits

...

16 Commits

Author SHA1 Message Date
oat 88fc57da82
changed up the zip package, fixed bad zip issues 2020-12-26 23:25:53 +03:00
oat 564d71c48f
okay 2020-12-26 23:03:34 +03:00
oat 4cbe252852
now no two users can have the same uuid 2020-10-12 21:13:45 +03:00
oat 0a150ad343
one more auth fix 2020-10-12 20:56:06 +03:00
oat f113c472fd
fixed a few issues with auth 2020-10-12 17:10:18 +03:00
oat 96facbf2e2
ok figured it out 2020-10-12 05:10:15 +03:00
oat df6c1545af
testing in prod time 2020-10-12 05:07:58 +03:00
oat e218255441
unhardcode base url (i think closes #3?) 2020-10-12 05:02:45 +03:00
oat 36f4da4a28
readme update 2020-10-12 04:15:11 +03:00
oat ef58af8a9a
user checking when user uploads 2020-10-12 03:39:44 +03:00
oat 795f983721
funny logins hooray oh yeah oh yeah 2020-10-12 03:27:47 +03:00
oat 6b07ef943d
fix up html formatting 2020-10-12 01:24:33 +03:00
oat 73d5962c5c
file uploading, now with the actual file 2020-10-12 01:21:32 +03:00
oat 1ed6dc6185
add chart data
steps, jumps, mines, etc
2020-09-04 18:02:42 +03:00
oat 64446588c9
schema updates
semi-final schemas, i think
2020-09-04 16:47:10 +03:00
jill dc88ad2b75 Merge pull request 'slightly better (but still temporary) frontend and /api' (#4) from mat/in-the-database-2:master into master
Reviewed-on: oat/in-the-database-2#4
closes #2
2020-09-04 04:43:26 +00:00
14 changed files with 677 additions and 369 deletions

3
.gitignore vendored
View File

@ -3,4 +3,5 @@ node_modules
built built
config/config.json config/config.json
*.log *.log
.env .env
storage/files/*

View File

@ -1,6 +1,8 @@
![in the database 2](./assets/full.png "in the database 2") ![in the database 2](./assets/full.png "in the database 2")
a database site for notitg modcharts, currently very very unfinished, basically just a boilerplate a database site for notitg modcharts, currently very very unfinished
you can login with discord, upload and download files, thats about it, but im still proud of it
## setup ## setup

BIN
assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

682
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,20 +12,24 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/express": "github:types/express", "@types/express": "github:types/express",
"@types/mongoose": "^5.7.36", "adm-zip": "^0.5.1",
"axios": "^0.20.0", "axios": "^0.20.0",
"connect-mongo": "^3.2.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-fileupload": "^1.2.0", "express-fileupload": "^1.2.0",
"mongoose": "^5.10.2", "express-session": "^1.17.1",
"mongoose": "^5.11.8",
"mongoose-int32": "^0.4.1", "mongoose-int32": "^0.4.1",
"node-stream-zip": "^1.11.3", "serve-favicon": "^2.5.0",
"typescript": "^4.0.2", "typescript": "^4.1.3",
"uuid": "^8.3.2",
"winston": "^3.3.3" "winston": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.0.1", "@types/express-session": "^1.17.3",
"@typescript-eslint/parser": "^4.0.1", "@typescript-eslint/eslint-plugin": "^4.11.0",
"eslint": "^7.8.1" "@typescript-eslint/parser": "^4.11.0",
"eslint": "^7.16.0"
} }
} }

View File

@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Index</title> <title>Index</title>
</head> </head>
<body> <body>
<h1>Hi</h1> <h1>Hi</h1>
</body> </body>
</html> </html>

View File

@ -1,36 +1,42 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>list</title> <title>list</title>
</head> </head>
<body> <body>
<div id="doc-list"> <div id="doc-list">
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/axios@0.20.0/dist/axios.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/axios@0.20.0/dist/axios.min.js"></script>
<script> <script>
axios.get('/api/list').then(({ data }) => { axios.get('/api/list').then(({ data }) => {
console.log(data); console.log(data);
const el = document.getElementById('doc-list'); const el = document.getElementById('doc-list');
for (const doc of data) { for (const doc of data) {
let p = document.createElement('p'); let p = document.createElement('p');
p.innerText = `${doc.artist} - ${doc.title} by ${doc.credit}`; p.innerHTML = `<b>${doc.artist} - ${doc.title}</b> by ${doc.credit}\nuploaded by ${doc.uploaderJSON.username}#${doc.uploaderJSON.discriminator}\n<a href="files/${doc.id}.zip">download</a>`;
el.insertAdjacentElement('beforeend', p);
let charts = document.createElement('ul'); if (doc.editable) {
for (const chart of doc.charts) { p.innerHTML += ` <a href="../${doc.id}/edit">edit</a>`
let l = document.createElement('li'); }
l.innerText = `${chart.difficulty} ${chart.rating} - ${chart.name}`
charts.insertAdjacentElement('beforeend', l); el.insertAdjacentElement('beforeend', p);
}
el.insertAdjacentElement('beforeend', charts); let charts = document.createElement('ul');
} for (const chart of doc.charts) {
}); let l = document.createElement('li');
</script> l.innerHTML = `${chart.difficulty} ${chart.rating} - <b>${chart.name}</b><br>` +
</body> `${chart.steps} steps, ${chart.mines} mines, ${chart.jumps} jumps, ${chart.hands} hands, ${chart.holds} holds, ${chart.rolls} rolls`
charts.insertAdjacentElement('beforeend', l);
}
el.insertAdjacentElement('beforeend', charts);
}
});
</script>
</body>
</html> </html>

View File

@ -19,7 +19,7 @@
const file = document.getElementById('file'); const file = document.getElementById('file');
if (file.files.length) { if (file.files.length) {
console.log(file.files[0]); console.log(file.files[0]);
const formData = new FormData(); const formData = new FormData();
formData.append('file', file.files[0]); formData.append('file', file.files[0]);
try { try {

View File

@ -1,3 +1,6 @@
import { User } from './schema';
import * as uuid from 'uuid';
const API_ENDPOINT = 'https://discord.com/api/v6'; const API_ENDPOINT = 'https://discord.com/api/v6';
const axios = require('axios').default; const axios = require('axios').default;
@ -5,6 +8,7 @@ const axios = require('axios').default;
export function run(app) { export function run(app) {
app.get('/discordauth', async (req, res) => { app.get('/discordauth', async (req, res) => {
const code = req.query.code; const code = req.query.code;
const url = `http://${req.headers.host}/discordauth`;
if (code) { if (code) {
try { try {
@ -13,7 +17,7 @@ export function run(app) {
client_secret: process.env.DISCORD_OAUTH_CLIENTSECRET, client_secret: process.env.DISCORD_OAUTH_CLIENTSECRET,
grant_type: 'authorization_code', grant_type: 'authorization_code',
code: code, code: code,
redirect_uri: 'http://localhost:8080/discordauth', redirect_uri: url,
scope: 'identify' scope: 'identify'
}); });
@ -28,12 +32,49 @@ export function run(app) {
authorization: `${postRes.data.token_type} ${postRes.data.access_token}` authorization: `${postRes.data.token_type} ${postRes.data.access_token}`
} }
}); });
res.send(`hi ${userInfo.data.username}#${userInfo.data.discriminator}<br><img src="https://media.discordapp.net/avatars/${userInfo.data.id}/${userInfo.data.avatar}.png">`);
const users = await User.find({id: String(userInfo.data.id)});
let userUuid = '';
if (users.length === 0) {
let newUuid = uuid.v4();
while (User.find({uuid: newUuid})[0]) {
newUuid = uuid.v4();
}
const newUser = new User({
id: String(userInfo.data.id),
createdAt: new Date(),
username: userInfo.data.username,
discriminator: userInfo.data.discriminator,
avatar: userInfo.data.avatar,
uuid: newUuid,
});
userUuid = newUser.get('uuid');
newUser.save();
} else {
const user = users[0];
userUuid = user.get('uuid');
user.set('id', String(userInfo.data.id));
user.set('username', userInfo.data.username);
user.set('discriminator', userInfo.data.discriminator);
user.set('avatar', userInfo.data.avatar);
}
req.session!.discord = userInfo.data;
req.session!.uuid = userUuid;
res.send(`logged in as ${userInfo.data.username}#${userInfo.data.discriminator}<br><img src="https://media.discordapp.net/avatars/${userInfo.data.id}/${userInfo.data.avatar}.png"><br>ur useruuid is ${userUuid}`);
} catch(err) { } catch(err) {
res.send(`whoooops<br>${err}`); res.send(`whoooops<br>${err}`);
console.error(err);
} }
} else { } else {
res.send(`<a href="https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_OAUTH_CLIENTID}&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fdiscordauth&response_type=code&scope=identify">Click here!!</a>`); res.send(`<a href="https://discord.com/api/oauth2/authorize?client_id=${process.env.DISCORD_OAUTH_CLIENTID}&redirect_uri=${encodeURI(url)}&response_type=code&scope=identify">Click here!!</a>`);
} }
}); });
} }

View File

@ -3,9 +3,12 @@ import * as mongoose from 'mongoose';
import * as fs from 'fs'; import * as fs from 'fs';
import * as winston from 'winston'; import * as winston from 'winston';
import * as fileUpload from 'express-fileupload'; import * as fileUpload from 'express-fileupload';
import * as session from 'express-session';
import * as favicon from 'serve-favicon';
const MongoStore = require('connect-mongo')(session);
import * as format from './lib/format'; import * as format from './lib/format';
import { File } from './schema'; import { File, User } from './schema';
import * as upload from './upload'; import * as upload from './upload';
import * as auth from './auth'; import * as auth from './auth';
@ -52,8 +55,22 @@ db.then(() => {
// @ts-ignore // @ts-ignore
app.use(express.urlencoded({extended: true})); app.use(express.urlencoded({extended: true}));
app.use(favicon('assets/icon.ico'));
app.use(fileUpload({limits: { fileSize: 50 * 1024 * 1024 }})); app.use(fileUpload({limits: { fileSize: 50 * 1024 * 1024 }}));
app.use(express.static('public', {extensions: ['html', 'htm']})); app.use(express.static('public', {extensions: ['html', 'htm']}));
app.use(express.static('storage', {extensions: ['zip']}));
app.use(session({
name: 'funnyuserdata',
secret: 'wenis',
store: new MongoStore({ mongooseConnection: mongoose.connection }),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 365 * 10, // 10 years
httpOnly: true,
sameSite: 'lax',
},
resave: false,
saveUninitialized: true
}));
app.use('/assets', express.static('assets')); app.use('/assets', express.static('assets'));
app.set('db', db); app.set('db', db);
@ -63,8 +80,23 @@ db.then(() => {
upload.run(app); upload.run(app);
auth.run(app); auth.run(app);
app.get('/api/list', async (req, res) => { // only for testing app.get('/api/list', async (req, res) => {
const docs = await File.find({}); const files = await File.find({});
const docs = [];
for (const doc of files) {
const d: any = doc.toJSON();
d.editable = false;
if (req.session) d.editable = req.session.uuid === d.uploader;
const user = await User.find({uuid: d.uploader});
if (user) {
d.uploaderJSON = user[0].toJSON(); // this is built upon 20 layers of metajank and i despise it
docs.push(d);
}
}
// TODO: filter out _id and __v? possibly more // TODO: filter out _id and __v? possibly more
res.send(docs); res.send(docs);
}); });

View File

@ -1,6 +1,4 @@
export function parseSM(data: string) { export function parseSM(data: string) {
data = data.replace(/[\n\r]/g,'');
// steps // steps
const difficulties = []; const difficulties = [];
const steps = data.split('#NOTES:'); const steps = data.split('#NOTES:');
@ -18,11 +16,25 @@ export function parseSM(data: string) {
diff.rating = Number(stepsSplit[3]); diff.rating = Number(stepsSplit[3]);
diff.radarvalues = stepsSplit[4].split(',').map(v => Number(v)); diff.radarvalues = stepsSplit[4].split(',').map(v => Number(v));
const chart = stepsSplit[5];
diff.rawChart = chart;
diff.steps = chart.split(/[124]/g).length - 1;
diff.mines = chart.split('M').length - 1;
diff.jumps = chart.split(/[124]0{0,2}[124]/g).length - 1;
diff.hands = chart.split(/[124]0{0,1}[124]0{0,1}[124]/g).length - 1;
diff.holds = chart.split('2').length - 1;
diff.rolls = chart.split('4').length - 1;
diff.steps -= diff.jumps; // jumps are counted as 1 step
difficulties.push(diff); difficulties.push(diff);
} }
} }
} }
data = data.replace(/[\n\r]/g,'');
// metadata // metadata
const lines = data.split(';').filter(l => l.startsWith('#')); const lines = data.split(';').filter(l => l.startsWith('#'));
const obj: any = {}; const obj: any = {};
@ -41,7 +53,7 @@ export function parseSM(data: string) {
const map = {}; const map = {};
for (const i in keys) { for (const i in keys) {
map[Number(keys[i])] = Number(values[i]); // afaik maps are only numbers? map[String(Number(keys[i])).replace('.', ',')] = Number(values[i]); // afaik maps are only numbers?
} }
value = map; value = map;

View File

@ -1,10 +0,0 @@
import * as fs from 'fs';
export function returnStatic(page) {
return (req, res) => {
fs.readFile(`src/html/${page}`, 'utf8', (err, data) => {
if (err) throw err;
res.send(data);
});
};
}

View File

@ -1,39 +1,118 @@
/* eslint-disable no-unused-vars */
import * as mongoose from 'mongoose'; import * as mongoose from 'mongoose';
const Schema = mongoose.Schema; const Schema = mongoose.Schema;
export enum SMVersion {
OPENITG,
FUCKEXE,
NOTITG_V1,
NOTITG_V2,
NOTITG_V3,
NOTITG_V3_1,
NOTITG_V4,
NOTITG_V4_0_1,
STEPMANIA_3_95,
STEPMANIA_5_0,
STEPMANIA_5_1,
STEPMANIA_5_2,
STEPMANIA_5_3,
}
const Sample = new Schema({ const Sample = new Schema({
start: {type: Number, default: 0}, start: {type: Number, default: 0},
length: {type: Number, default: 0} length: {type: Number, default: 0}
}); });
const UserRating = new Schema({
rating: {type: Number, default: 0},
createdAt: Date,
user: {type: String, default: '00000000-0000-4000-a000-000000000000'}
});
const Chart = new Schema({ const Chart = new Schema({
type: {type: String, default: 'dance-single'}, type: {type: String, default: 'dance-single'},
name: {type: String, default: ''}, name: {type: String, default: ''},
difficulty: {type: String, default: 'Challenge'}, difficulty: {type: String, default: 'Challenge'},
radarvalues: [Number],
rating: {type: Number, default: 0}, rating: {type: Number, default: 0},
radarvalues: [Number] ratingsVote: {type: [UserRating], default: []},
spoilered: {type: Boolean, default: false},
hidden: {type: Boolean, default: false},
steps: {type: Number, default: 0},
mines: {type: Number, default: 0},
jumps: {type: Number, default: 0},
hands: {type: Number, default: 0},
holds: {type: Number, default: 0},
rolls: {type: Number, default: 0},
});
const Comment = new Schema({
author: {type: String, default: '00000000-0000-4000-a000-000000000000'},
createdAt: Date,
content: {type: String, default: ''}
}); });
const FileSchema = new Schema({ const FileSchema = new Schema({
id: {type: Number, default: 0},
title: {type: String, default: 'unknown'}, title: {type: String, default: 'unknown'},
titleTranslit: String, titleTranslit: String,
artist: {type: String, default: 'unknown'}, artist: {type: String, default: 'unknown'},
artistTranslit: String, artistTranslit: String,
subtitle: String, subtitle: String,
subtitleTranslit: String, subtitleTranslit: String,
credit: String, credit: String,
uploader: {type: String, default: '00000000-0000-4000-a000-000000000000'}, uploader: {type: String, default: '00000000-0000-4000-a000-000000000000'},
sample: Sample, sample: Sample,
bpms: {type: Object, default: {'0': 0}}, bpms: {type: Object, default: {'0': 0}},
charts: [Chart]
charts: {type: [Chart], default: []},
description: {type: String, default: ''},
createdAt: Date,
smVersion: {type: Number, default: 0}, // see SMVersion enum
ytLink: String,
customLink: String,
hidden: {type: Boolean, default: false},
comments: {type: [Comment], default: []},
}); });
export const File = mongoose.model('File', FileSchema); export const File = mongoose.model('File', FileSchema);
const UserSchema = new Schema({ // this is pretty much just a discord user lol const UserSchema = new Schema({ // this is pretty much just a discord user lol
id: String, // cus longass number id: {type: String, default: 'notgiven!!!!!!!!!!!!'}, // discord id, cus longass number
approved: Boolean createdAt: Date,
// caching
username: {type: String, default: 'User'},
discriminator: {type: String, default: '0000'},
avatar: String,
// used internally
uuid: {type: String, default: '00000000-0000-4000-a000-000000000000'},
approvedUpload: {type: Boolean, default: false},
approvedRate: {type: Boolean, default: false},
approvedComment: {type: Boolean, default: false},
}); });
export const User = mongoose.model('User', UserSchema); export const User = mongoose.model('User', UserSchema);
const PackSchema = new Schema({
author: {type: String, default: '00000000-0000-4000-a000-000000000000'},
files: {type: [Number], default: []}, // ids
name: {type: String, default: 'Pack'},
description: {type: String, default: ''},
createdAt: Date,
hidden: {type: Boolean, default: false},
});
export const Pack = mongoose.model('Pack', PackSchema);

View File

@ -1,54 +1,75 @@
import { tmpdir } from 'os'; import { tmpdir } from 'os';
import * as fs from 'fs'; import * as fs from 'fs';
const StreamZip = require('node-stream-zip'); import * as AdmZip from 'adm-zip';
import { returnStatic } from './lib/util';
import { parseSM } from './lib/smparse'; import { parseSM } from './lib/smparse';
import { File } from './schema'; import { File, User } from './schema';
export function run(app) { export function run(app) {
const logger = app.get('logger'); const logger = app.get('logger');
app.post('/api/upload', async (req, res) => { // only for testing, very abusable app.post('/api/upload', async (req, res) => { // only for testing, very abusable
if (!req.files) return res.status(400).send('No files were given'); if (!req.files) return res.status(400).send('No files were given');
if (!req.session.uuid) return res.status(401).send('Not authorized, use /discordauth');
const user = (await User.find({uuid: req.session.uuid}))[0];
if (!user) return res.status(401).send('User doesn\'t exist, try re-logging in');
if (!user.get('approvedUpload')) return res.status(403).send('Your account is not allowed to upload files! Contact a moderator to verify your account');
const file = req.files.file; const file = req.files.file;
if (file.mimetype !== 'application/zip' && file.mimetype !== 'application/x-zip-compressed') return res.status(400).send('Invalid filetype'); if (file.mimetype !== 'application/zip' && file.mimetype !== 'application/x-zip-compressed') return res.status(400).send('Invalid filetype');
const dir = tmpdir() + '/' + file.md5; const dir = tmpdir() + '/' + file.md5;
fs.writeFile(dir, file.data, (err) => { fs.writeFile(dir, file.data, async (err) => {
if (err) throw err; if (err) throw err;
const zip = new StreamZip({ try {
file: dir, const zip = new AdmZip(dir);
storeEntries: true const zipEntries = zip.getEntries();
});
zip.on('ready', () => { const smFile: any = Object.values(zipEntries).find((f: any) =>
const smFile = Object.values(zip.entries()).find((f: any) => !f.isDirectory && (f.entryName.endsWith('.sm'))
!f.isDirectory && (f.name.endsWith('.sm'))
); );
if (!smFile) { if (!smFile) {
res.status(400).send('No .sm found'); res.status(400).send('No .sm found');
} else { } else {
const data = zip.entryDataSync((smFile as any).name); const data = smFile.getData().toString('utf8');
const chart = parseSM(data.toString()); const chart = parseSM(data.toString());
logger.info(`${chart.artist} - ${chart.title} was just uploaded`); logger.info(`${chart.artist} - ${chart.title} was just uploaded`);
const file = new File(chart); let id = 0;
file.save(); for (const f of await File.find({})) {
id = Math.max(Number(f.id), id);
}
chart.id = id + 1;
// TODO: filter out _id and __v? possibly more chart.uploader = req.session.uuid;
res.send(chart);
chart.createdAt = new Date();
fs.writeFile('./storage/files/' + (id + 1) + '.zip', file.data, (err) => {
if (err) throw err;
const file = new File(chart);
file.save();
// TODO: filter out _id and __v? possibly more
res.send(chart);
});
} }
zip.close();
fs.unlink(dir, (err) => { fs.unlink(dir, (err) => {
if (err) throw err; if (err) throw err;
}); });
}); } catch(err) {
logger.error(err.toString());
console.error(err);
res.status(400);
res.send(err.toString());
}
}); });
}); });
} }