Commit bdddedf3 authored by Geovanny's avatar Geovanny

Nav bar done and some login

parent 541ddfa2
...@@ -3,6 +3,7 @@ server/node_modules/ ...@@ -3,6 +3,7 @@ server/node_modules/
server/.env server/.env
discord/.env discord/.env
discord/__pycache__ discord/__pycache__
web/node_modules
*.pyc *.pyc
__pycache__ __pycache__
.vscode .vscode
\ No newline at end of file
...@@ -45,7 +45,7 @@ class UnitManager(commands.Cog): ...@@ -45,7 +45,7 @@ class UnitManager(commands.Cog):
except asyncio.TimeoutError: except asyncio.TimeoutError:
return await ctx.send('Operation aborted') return await ctx.send('Operation aborted')
if msg.content.lower() != 'yes': if not(msg.content.lower() == 'yes' or msg.content.lower() == 'y'):
return await ctx.send('Operation aborted') return await ctx.send('Operation aborted')
try: try:
await Api.put('/unit/{0}'.format(data['id']), params) await Api.put('/unit/{0}'.format(data['id']), params)
...@@ -91,13 +91,13 @@ class UnitManager(commands.Cog): ...@@ -91,13 +91,13 @@ class UnitManager(commands.Cog):
embed.add_field(name='ID', value=unit_data.id, inline=True) embed.add_field(name='ID', value=unit_data.id, inline=True)
embed.add_field(name='Name', value=unit_data.name) embed.add_field(name='Name', value=unit_data.name)
embed.add_field(name='Type', value=unit_data.type, inline=True) embed.add_field(name='Type', value=unit_data.unit_type, inline=True)
embed.add_field(name='HP', value=unit_data.hp)
embed.add_field(name='Stars', value=(unit_data.stars/2)) embed.add_field(name='Stars', value=(unit_data.stars/2))
embed.add_field(name='Speed', value=unit_data.speed, inline=True) embed.add_field(name='Speed', value=unit_data.speed, inline=True)
embed.add_field(name='Range', value=unit_data.range) embed.add_field(name='Range', value=unit_data.unit_range)
embed.add_field(name='Ammo', value=unit) embed.add_field(name='Ammo', value=unit_data.ammo)
embed.add_field(name='Leadership Cost', value=unit_data.leadership) embed.add_field(name='Troop Count', value=unit_data.tc, inline=True)
embed.add_field(name='Troop Count', value=unit_data.troop_count, inline=True)
embed.add_field(name='Piercing AP', value=unit_data.pap, inline=True) embed.add_field(name='Piercing AP', value=unit_data.pap, inline=True)
embed.add_field(name='Piercing Dg', value=unit_data.pd, inline=True) embed.add_field(name='Piercing Dg', value=unit_data.pd, inline=True)
embed.add_field(name='Piercing Df', value=unit_data.pdf, inline=True) embed.add_field(name='Piercing Df', value=unit_data.pdf, inline=True)
...@@ -108,6 +108,8 @@ class UnitManager(commands.Cog): ...@@ -108,6 +108,8 @@ class UnitManager(commands.Cog):
embed.add_field(name='Blunt Dg', value=unit_data.bd, inline=True) embed.add_field(name='Blunt Dg', value=unit_data.bd, inline=True)
embed.add_field(name='Blunt Df', value=unit_data.bdf, inline=True) embed.add_field(name='Blunt Df', value=unit_data.bdf, inline=True)
embed.add_field(name='Labour', value=unit_data.labour, inline=True) embed.add_field(name='Labour', value=unit_data.labour, inline=True)
embed.add_field(name='Hero Level', value=unit_data.hl, inline=True)
embed.add_field(name='Leadership Cost', value=unit_data.ld)
await ctx.send(embed=embed) await ctx.send(embed=embed)
...@@ -127,7 +129,7 @@ class UnitManager(commands.Cog): ...@@ -127,7 +129,7 @@ class UnitManager(commands.Cog):
await self.createUnitTable(ctx, units_data) await self.createUnitTable(ctx, units_data)
async def createUnitTable(self, ctx, data): async def createUnitTable(self, ctx, data):
return await EmbedStyle.createPages(self.bot, ctx, data, 5, self.createUnitPage) return await EmbedStyle.createPages(self.bot, ctx, data, 10, self.createUnitPage)
def createUnitPage(self, data, minI, maxI): def createUnitPage(self, data, minI, maxI):
embed = discord.Embed(color=0x19212d) embed = discord.Embed(color=0x19212d)
......
class Unit(object): class Unit(object):
def __init__(self, id, name, type, stars=0, hp=0, pap=0, pd=0, sap=0, sd=0, bap=0, bd=0, pdf=0, sdf=0, bdf=0, leadership=0, troop_count=0, hero_level=0, speed=0, range=0, ammo=0, labour=0, def __init__(self, id, name, unit_type, stars=0, hp=0, pap=0, pd=0, sap=0, sd=0, bap=0, bd=0, pdf=0, sdf=0, bdf=0, ld=0, tc=0, hl=0, speed=0, unit_range=0, ammo=0, labour=0,
img=None, img=None,
vet_img=None, vet_img=None,
unit_level=0, unit_level=0,
elite_flg=None): elite_flg=None):
if unit_type=='':
unit_type = 'None'
self.id = id self.id = id
self.name = name self.name = name
self.type = type self.unit_type = unit_type
self.stars = stars self.stars = stars
self.hp = hp self.hp = hp
self.pap = pap self.pap = pap
...@@ -19,11 +21,11 @@ class Unit(object): ...@@ -19,11 +21,11 @@ class Unit(object):
self.pdf = pdf self.pdf = pdf
self.sdf = sdf self.sdf = sdf
self.bdf = bdf self.bdf = bdf
self.leadership = leadership self.ld = ld
self.troop_count = troop_count self.tc = tc
self.hero_level = hero_level self.hl = hl
self.speed = speed self.speed = speed
self.range = range self.unit_range = unit_range
self.ammo = ammo self.ammo = ammo
self.labour = labour self.labour = labour
self.img = img self.img = img
...@@ -46,4 +48,4 @@ class Unit(object): ...@@ -46,4 +48,4 @@ class Unit(object):
} }
def __str__(self): def __str__(self):
return '[{0}] {1}\t| {2}\t| {3}'.format(self.id, self.name, self.type, self.stars) return '[{0}] {1}\t| {2}\t| {3}'.format(self.id, self.name, self.unit_type, self.stars)
\ No newline at end of file \ No newline at end of file
from discord_argparse import * from discord_argparse import *
insert_param_converter = ArgumentConverter( insert_param_converter = ArgumentConverter(
type = OptionalArgument( unit_type = OptionalArgument(
str, str,
doc='Type of Unit', doc='Type of Unit',
), ),
...@@ -62,10 +62,10 @@ insert_param_converter = ArgumentConverter( ...@@ -62,10 +62,10 @@ insert_param_converter = ArgumentConverter(
doc='Hero level required', doc='Hero level required',
), ),
speed = OptionalArgument( speed = OptionalArgument(
int, float,
doc='Speed of unit', doc='Speed of unit',
), ),
range = OptionalArgument( unit_range = OptionalArgument(
int, int,
doc='Range of unit', doc='Range of unit',
), ),
...@@ -74,7 +74,7 @@ insert_param_converter = ArgumentConverter( ...@@ -74,7 +74,7 @@ insert_param_converter = ArgumentConverter(
doc='Ammo if unit', doc='Ammo if unit',
), ),
labour = OptionalArgument( labour = OptionalArgument(
int, float,
doc='Resourse collection labour', doc='Resourse collection labour',
), ),
img = OptionalArgument( img = OptionalArgument(
...@@ -92,7 +92,7 @@ modify_param_converter = ArgumentConverter( ...@@ -92,7 +92,7 @@ modify_param_converter = ArgumentConverter(
str, str,
doc='New name', doc='New name',
), ),
type = OptionalArgument( unit_type = OptionalArgument(
str, str,
doc='Type of Unit', doc='Type of Unit',
), ),
...@@ -153,10 +153,10 @@ modify_param_converter = ArgumentConverter( ...@@ -153,10 +153,10 @@ modify_param_converter = ArgumentConverter(
doc='Hero level required', doc='Hero level required',
), ),
speed = OptionalArgument( speed = OptionalArgument(
int, float,
doc='Speed of unit', doc='Speed of unit',
), ),
range = OptionalArgument( unit_range = OptionalArgument(
int, int,
doc='Range of unit', doc='Range of unit',
), ),
...@@ -165,7 +165,7 @@ modify_param_converter = ArgumentConverter( ...@@ -165,7 +165,7 @@ modify_param_converter = ArgumentConverter(
doc='Ammo if unit', doc='Ammo if unit',
), ),
labour = OptionalArgument( labour = OptionalArgument(
int, float,
doc='Resourse collection labour', doc='Resourse collection labour',
), ),
img = OptionalArgument( img = OptionalArgument(
......
...@@ -48,6 +48,8 @@ class UserManager(commands.Cog): ...@@ -48,6 +48,8 @@ class UserManager(commands.Cog):
msg = await self.bot.wait_for('message', timeout=30.0, check=check) msg = await self.bot.wait_for('message', timeout=30.0, check=check)
except asyncio.TimeoutError: except asyncio.TimeoutError:
return await ctx.send('Operation aborted') return await ctx.send('Operation aborted')
if not(msg.content.lower() == 'yes' or msg.content.lower() == 'y'):
return await ctx.send('Operation aborted')
try: try:
session = await self.bot.getUserSession(ctx.message.author) session = await self.bot.getUserSession(ctx.message.author)
await Api.deleteSession('/user/unit/{0}'.format(data['id']), session) await Api.deleteSession('/user/unit/{0}'.format(data['id']), session)
...@@ -76,6 +78,8 @@ class UserManager(commands.Cog): ...@@ -76,6 +78,8 @@ class UserManager(commands.Cog):
msg = await self.bot.wait_for('message', timeout=30.0, check=check) msg = await self.bot.wait_for('message', timeout=30.0, check=check)
except asyncio.TimeoutError: except asyncio.TimeoutError:
return await ctx.send('Operation aborted') return await ctx.send('Operation aborted')
if not(msg.content.lower() == 'yes' or msg.content.lower() == 'y'):
return await ctx.send('Operation aborted')
try: try:
session = await self.bot.getUserSession(ctx.message.author) session = await self.bot.getUserSession(ctx.message.author)
body = params body = params
...@@ -104,11 +108,12 @@ class UserManager(commands.Cog): ...@@ -104,11 +108,12 @@ class UserManager(commands.Cog):
msg = await self.bot.wait_for('message', timeout=30.0, check=check) msg = await self.bot.wait_for('message', timeout=30.0, check=check)
except asyncio.TimeoutError: except asyncio.TimeoutError:
return await ctx.send('Operation aborted') return await ctx.send('Operation aborted')
if not(msg.content.lower() == 'yes' or msg.content.lower() == 'y'):
return await ctx.send('Operation aborted')
try: try:
session = await self.bot.getUserSession(ctx.message.author) session = await self.bot.getUserSession(ctx.message.author)
body = params body = params
body['unit_id'] = data['id'] body['unit_id'] = data['id']
print(body)
await Api.postSession('/user/unit', body, session) await Api.postSession('/user/unit', body, session)
return await ctx.send('Unit Assigned Successfully') return await ctx.send('Unit Assigned Successfully')
except ApiError as error: except ApiError as error:
...@@ -135,7 +140,7 @@ class UserManager(commands.Cog): ...@@ -135,7 +140,7 @@ class UserManager(commands.Cog):
for i in range(minI, maxI): for i in range(minI, maxI):
if i < len(data): if i < len(data):
unit = data[i] unit = data[i]
units_str+= '[{0}] {1}\t| {2}\t| {3}\t| {4}\n'.format(unit.id, unit.name, unit.type, unit.stars, unit.unit_level) units_str+= '[{0}] {1}\t| {2}\t| {3}\t| {4}\n'.format(unit.id, unit.name, unit.unit_type, unit.stars, unit.unit_level)
embed.add_field(name='id\t| name\t| type\t| stars\t| level', value=units_str) embed.add_field(name='id\t| name\t| type\t| stars\t| level', value=units_str)
......
...@@ -5,7 +5,7 @@ param_converter = ArgumentConverter( ...@@ -5,7 +5,7 @@ param_converter = ArgumentConverter(
str, str,
doc='Level of Unit', doc='Level of Unit',
), ),
eltie_flg = OptionalArgument( elite_flg = OptionalArgument(
bool, bool,
doc='Is Unit Elite', doc='Is Unit Elite',
) )
......
...@@ -4,13 +4,20 @@ const Koa = require('koa'); ...@@ -4,13 +4,20 @@ const Koa = require('koa');
const Router = require('@koa/router'); const Router = require('@koa/router');
const bodyParser = require('koa-body'); const bodyParser = require('koa-body');
const session = require('koa-session') const session = require('koa-session')
const serve = require('koa-static');
const logger = require('koa-logger')
const userRouter = require('./user/route'); const getUser = require('./user/model');
const [userRouter, userAuthRouter] = require('./user/route');
const unitRouter = require('./unit/route'); const unitRouter = require('./unit/route');
const SESS_CONFIG = require('./session_config'); const SESS_CONFIG = require('./session_config');
const ENV = require('./settings') const ENV = require('./settings')
const app = new Koa(); const app = new Koa();
const router = new Router(); const router = new Router();
const authRouter = new Router();
app.use(serve(`${__dirname}/../web`))
app.use(logger())
app.keys = [ENV.SESS_KEY] app.keys = [ENV.SESS_KEY]
app.use(bodyParser()); app.use(bodyParser());
...@@ -19,8 +26,30 @@ app.use(session(SESS_CONFIG, app)); ...@@ -19,8 +26,30 @@ app.use(session(SESS_CONFIG, app));
router.use('/unit', unitRouter.routes(), unitRouter.allowedMethods()); router.use('/unit', unitRouter.routes(), unitRouter.allowedMethods());
router.use('/user', userRouter.routes(), userRouter.allowedMethods()); router.use('/user', userRouter.routes(), userRouter.allowedMethods());
//app.use(auth()); authRouter.use('/user', userAuthRouter.routes(), userAuthRouter.allowedMethods());
app.use(router.routes()).use(router.allowedMethods()); app.use(router.routes()).use(router.allowedMethods());
// Make sure user is authorized
app.use(async (context, next) => {
if(context.session.user_id){
await next();
console.log(context.status)
} else {
context.response.status = 401;
}
});
// Add user to context
app.use(async (context, next) => {
const user = getUser.getUserFromId(context.session.user_id);
if(user){
context.user = user;
await next();
} else {
context.response.status = 403;
}
});
app.use(authRouter.routes()).use(authRouter.allowedMethods());
app.listen(3000, () => console.log('Server Started')); app.listen(3000, () => console.log('Server Started'));
\ No newline at end of file
...@@ -57,6 +57,14 @@ ...@@ -57,6 +57,14 @@
"negotiator": "0.6.2" "negotiator": "0.6.2"
} }
}, },
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"requires": {
"color-convert": "^1.9.0"
}
},
"any-promise": { "any-promise": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
...@@ -100,6 +108,16 @@ ...@@ -100,6 +108,16 @@
"ylru": "^1.2.0" "ylru": "^1.2.0"
} }
}, },
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"co": { "co": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
...@@ -116,6 +134,19 @@ ...@@ -116,6 +134,19 @@
"type-is": "^1.6.14" "type-is": "^1.6.14"
} }
}, },
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"content-disposition": { "content-disposition": {
"version": "0.5.3", "version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
...@@ -211,6 +242,11 @@ ...@@ -211,6 +242,11 @@
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
}, },
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"formidable": { "formidable": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz",
...@@ -226,6 +262,11 @@ ...@@ -226,6 +262,11 @@
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
"integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=" "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ="
}, },
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"http-assert": { "http-assert": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz", "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz",
...@@ -247,6 +288,11 @@ ...@@ -247,6 +288,11 @@
"toidentifier": "1.0.0" "toidentifier": "1.0.0"
} }
}, },
"humanize-number": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/humanize-number/-/humanize-number-0.0.2.tgz",
"integrity": "sha1-EcCvakcWQ2M1iFiASPF5lUFInBg="
},
"iconv-lite": { "iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
...@@ -388,6 +434,17 @@ ...@@ -388,6 +434,17 @@
} }
} }
}, },
"koa-logger": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/koa-logger/-/koa-logger-3.2.1.tgz",
"integrity": "sha512-MjlznhLLKy9+kG8nAXKJLM0/ClsQp/Or2vI3a5rbSQmgl8IJBQO0KI5FA70BvW+hqjtxjp49SpH2E7okS6NmHg==",
"requires": {
"bytes": "^3.1.0",
"chalk": "^2.4.2",
"humanize-number": "0.0.2",
"passthrough-counter": "^1.0.0"
}
},
"koa-send": { "koa-send": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.0.tgz", "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.0.tgz",
...@@ -526,6 +583,11 @@ ...@@ -526,6 +583,11 @@
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
}, },
"passthrough-counter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passthrough-counter/-/passthrough-counter-1.0.0.tgz",
"integrity": "sha1-GWfZ5m2lcrXAI8eH2xEqOHqxZvo="
},
"path-is-absolute": { "path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
...@@ -657,6 +719,14 @@ ...@@ -657,6 +719,14 @@
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
}, },
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"requires": {
"has-flag": "^3.0.0"
}
},
"thenify": { "thenify": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz",
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
"fs": "0.0.1-security", "fs": "0.0.1-security",
"koa": "^2.11.0", "koa": "^2.11.0",
"koa-body": "^4.1.1", "koa-body": "^4.1.1",
"koa-logger": "^3.2.1",
"koa-session": "^5.13.1", "koa-session": "^5.13.1",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"promise-mysql": "^4.1.3" "promise-mysql": "^4.1.3"
......
...@@ -4,7 +4,7 @@ const db = require('../database/database'); ...@@ -4,7 +4,7 @@ const db = require('../database/database');
const unitModel = {}; const unitModel = {};
const unit_columns = ['name', 'type', 'stars', 'hp', 'pap', 'pd', 'sap', 'sd', 'bap', 'bp', 'pdf', 'sdf', 'bdf', 'leadership', 'troop_count', 'hero_level', 'speed', 'range', 'ammo', 'labour', 'img', 'vet_img']; const unit_columns = ['name', 'unit_type', 'stars', 'hp', 'pap', 'pd', 'sap', 'sd', 'bap', 'bp', 'pdf', 'sdf', 'bdf', 'ld', 'tc', 'hl', 'speed', 'unit_range', 'ammo', 'labour', 'img', 'vet_img'];
unitModel.getAll = async () =>{ unitModel.getAll = async () =>{
const sql_text = 'SELECT * FROM units ORDER BY name ASC;'; const sql_text = 'SELECT * FROM units ORDER BY name ASC;';
......
...@@ -9,12 +9,12 @@ const userModel = {}; ...@@ -9,12 +9,12 @@ const userModel = {};
const uu_columns = ['unit_level', 'elite_flg']; const uu_columns = ['unit_level', 'elite_flg'];
userModel.getUserFromId = async (id) => { userModel.getUserFromId = async (id) => {
const sql_text = 'SELECT TOP 1 id, discord_id, house_id, leadership FROM users WHERE id = ?;'; const sql_text = 'SELECT id, discord_id, house_id, leadership FROM users WHERE id = ? LIMIT 1;';
const data = await db.con.query(sql_text,[id]); const data = await db.con.query(sql_text,[id]);
return data[0]; return data[0];
} }
userModel.getUserFromDiscord = async (discordId) =>{ userModel.getUserFromDiscord = async (discordId) =>{
const sql_text = 'SELECT id, discord_id, house_id, leadership FROM users WHERE discord_id = ?;'; const sql_text = 'SELECT id, discord_id, house_id, leadership FROM users WHERE discord_id = ? LIMIT 1;';
const data = await db.con.query(sql_text, [discordId]); const data = await db.con.query(sql_text, [discordId]);
return data[0]; return data[0];
} }
......
...@@ -4,14 +4,9 @@ const Koa = require('koa'); ...@@ -4,14 +4,9 @@ const Koa = require('koa');
const Router = require('@koa/router'); const Router = require('@koa/router');
const router = new Router(); const router = new Router();
const authRouter = new Router();
const userModel = require('./model'); const userModel = require('./model');
function checkUser(context){
if(!context.session.user_id){
context.throw(401, 'No User Logged')
}
}
router.post('/d-login', async (context, next) =>{ router.post('/d-login', async (context, next) =>{
if(context.session.user_id && userModel.getUserFromId(context.session.user_id)){ if(context.session.user_id && userModel.getUserFromId(context.session.user_id)){
context.throw(400, 'User is Already Logged In') context.throw(400, 'User is Already Logged In')
...@@ -34,8 +29,43 @@ router.post('/d-login', async (context, next) =>{ ...@@ -34,8 +29,43 @@ router.post('/d-login', async (context, next) =>{
} }
}); });
router.get('/units', async (context, next) => {
checkUser(context); router.post('/discord-register', async(context, next) =>{
const body = context.request.body;
if(!body || !body.discordId){
context.throw(422, 'No Discord Id')
}
const discord_id = body.discordId;
if(context.user && !context.user.discord_id){
try{
await userModel.addDiscordIdToUser(context.user.id, discord_id);
context.status = 204;
}catch(error){
console.log(error);
context.throw(422, 'Invalid Discord Id')
}
}
let user = undefined;
try{
user = await userModel.getUserFromDiscord(discord_id);
}catch(error){
console.log(error);
context.throw(422, 'Invalid Discord Id')
}
if(!user){
try{
await userModel.createUserWithDiscord(discord_id);
context.status = 204;
}catch(error){
context.throw(422, 'Failed to create user with Discord Id')
}
}else{
context.throw(422, 'User already exists')
}
});
authRouter.get('/units', async (context, next) => {
try{ try{
const data = await userModel.getUserUnits(context.session.user_id); const data = await userModel.getUserUnits(context.session.user_id);
if(data.length===1 && data[0].id===null){ if(data.length===1 && data[0].id===null){
...@@ -49,8 +79,7 @@ router.get('/units', async (context, next) => { ...@@ -49,8 +79,7 @@ router.get('/units', async (context, next) => {
} }
}); });
router.get('/unit/:term', async (context, next) =>{ authRouter.get('/unit/:term', async (context, next) =>{
checkUser(context);
try{ try{
const data = await userModel.getUserUnit(context.session.user_id, context.params.term) const data = await userModel.getUserUnit(context.session.user_id, context.params.term)
context.response.body = data; context.response.body = data;
...@@ -61,8 +90,7 @@ router.get('/unit/:term', async (context, next) =>{ ...@@ -61,8 +90,7 @@ router.get('/unit/:term', async (context, next) =>{
} }
}) })
router.post('/unit', async (context, next) => { authRouter.post('/unit', async (context, next) => {
checkUser(context);
try{ try{
const body = context.request.body; const body = context.request.body;
if(!body.unit_id){ if(!body.unit_id){
...@@ -79,8 +107,7 @@ router.post('/unit', async (context, next) => { ...@@ -79,8 +107,7 @@ router.post('/unit', async (context, next) => {
} }
}) })
router.put('/unit/:unit_id', async (context, next) =>{ authRouter.put('/unit/:unit_id', async (context, next) =>{
checkUser(context);
try{ try{
const body = context.request.body; const body = context.request.body;
if(!body){ if(!body){
...@@ -94,8 +121,7 @@ router.put('/unit/:unit_id', async (context, next) =>{ ...@@ -94,8 +121,7 @@ router.put('/unit/:unit_id', async (context, next) =>{
} }
}) })
router.delete('/unit/:unit_id', async (context, next) =>{ authRouter.delete('/unit/:unit_id', async (context, next) =>{
checkUser(context);
try{ try{
await userModel.deleteUserUnit(context.session.user_id, context.params.unit_id); await userModel.deleteUserUnit(context.session.user_id, context.params.unit_id);
context.status = 204; context.status = 204;
...@@ -105,39 +131,5 @@ router.delete('/unit/:unit_id', async (context, next) =>{ ...@@ -105,39 +131,5 @@ router.delete('/unit/:unit_id', async (context, next) =>{
} }
}) })
router.post('/discord-register', async(context, next) =>{
const body = context.request.body;
if(!body || !body.discordId){
context.throw(422, 'No Discord Id')
}
const discord_id = body.discordId;
if(context.user && !context.user.discord_id){
try{
await userModel.addDiscordIdToUser(context.user.id, discord_id);
context.status = 204;
}catch(error){
console.log(error);
context.throw(422, 'Invalid Discord Id')
}
}
let user = undefined;
try{
data = await userModel.getUserFromDiscord(discord_id);
}catch(error){
console.log(error);
context.throw(422, 'Invalid Discord Id')
}
if(!user){
try{
await userModel.createUserWithDiscord(discord_id);
context.status = 204;
}catch(error){
context.throw(422, 'Failed to create user with Discord Id')
}
}else{
context.throw(422, 'User already exists')
}
});
module.exports = router; module.exports = [router, authRouter];
\ No newline at end of file \ No newline at end of file
@font-face {
font-family: 'Font Awesome 5 Brands';
font-style: normal;
font-weight: 400;
font-display: block;
src: url("/fontawesome/fa-brands-400.woff2") format("woff2");
}
@font-face {
font-family: 'Font Awesome 5 Free';
font-style: normal;
font-weight: 400;
font-display: block;
src: url("/fontawesome/fa-regular-400.woff2") format("woff2");
}
@font-face {
font-family: 'Font Awesome 5 Free';
font-style: normal;
font-weight: 900;
font-display: block;
src: url("/fontawesome/fa-solid-900.woff2") format("woff2");
}
@mixin icon($icon_code, $style){
&::before {
font-family: 'Font Awesome 5 Free';
content: $icon_code;
display: inline-block;
@if $style == "solid" {
font-weight: 900;
} @else if $style == "regular" {
font-weight: 400;
}
@content;
}
}
@keyframes fa-spin {
0% { transform: rotate( 0deg); }
100% { transform: rotate(360deg); }
}
@mixin spin_icon{
animation: fa-spin 2s linear infinite;
}
\ No newline at end of file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf8"/>
<link rel="stylesheet" href="./stylesheets/main.css"/>
<script src="sugar.js"></script>
<script>Sugar.extend()</script>
<script src="main.js" type="module"></script>
</head>
<body>
<nav-placeholder></nav-placeholder>
<script src="/navbar/navbar.js"></script>
<h1>STUFF IN HERE</h1>
</body>
</html>
import Sync from './sync'
class LoginPageController{
constructor(view){
this.view = view;
this.sync = new Sync();
this.view.addEventListener('login_attempt', this.login(event.detail.credentials));
this.view.addEventListener('register_attempt', this.login(event.detail.credentials))
}
async login(credentials){
try{
const user_data = await this.sync.login(credentials);
localStorage.setItem('username', user_data.user_name);
location.replace('/');
}catch(error){
console.log(error);
alert('Failed to login')
}
}
async login(credentials){
try{
const user_data = await this.sync.register(credentials);
localStorage.setItem('username', user_data.user_name);
location.replace('/');
}catch(error){
console.log(error);
alert('Failed to register')
}
}
}
export default LoginPageController;
\ No newline at end of file
<!doctype html>
<html lang="en">
<head>
<meta charset="utf8"/>
<link rel="stylesheet" href="./stylesheets/main.css"/>
<script src="sugar.js"></script>
<script>Sugar.extend()</script>
<script src="/main.js" type="module"></script>
</head>
<body>
<nav-placeholder></nav-placeholder>
<script src="/navbar/navbar.js"></script>
<content-body>
<center-div>
<div class="tab">
<button class="tablinks active" onclick="changeTab(event, 'Login')">Login</button>
<button class="tablinks" onclick="changeTab(event, 'Register')">Register</button>
</div>
<!-- Tab content -->
<div id="Login" class="tabcontent" style="display: block;">
<login-form>
<input type="text" name="login" placeholder="Login"/>
<input type="password" name="password" placeholder="********"/>
<button id="login_button">Log In</button>
</login-form>
</div>
<div id="Register" class="tabcontent">
<h3>Paris</h3>
<p>Paris is the capital of France.</p>
</div>
</center-div>
</content-body>
<script>
function changeTab(evt, cityName) {
// Declare all variables
var i, tabcontent, tablinks;
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
// Show the current tab, and add an "active" class to the button that opened the tab
document.getElementById(cityName).style.display = "block";
evt.currentTarget.className += " active";
}
</script>
</body>
</html>
class LoginView extends EventTarget {
constructor(element){
super()
this.element = element
this.login_field = this.element.querySelector("[name=login]")
this.password_field = this.element.querySelector("[name=password]")
this.login_button = this.element.querySelector("#login_button")
this.login_button.addEventListener("click", () => {
this.dispatchEvent(new CustomEvent("login_attempt", {detail: {
login: this.login_field.value,
password: this.password_field.value }
}))
})
}
show(){
this.element.style.display = ""
}
hide(){
this.element.style.display = "none"
}
}
export default LoginView
\ No newline at end of file
class Server{
async login(credentials){
const login_response = await fetch('/user/login', {
method: "POST",
body: JSON.stringify(credentials),
headers: {
'Content-Type': 'application/json'
}
})
if(!login_response.ok){
throw new Error(`Login failed with ${login_response.status}`)
}
const user_data = await login_response.json();
return user_data;
}
async register(credentials){
const register_response = await fetch('/user/register', {
method: "POST",
body: JSON.stringify(credentials),
headers: {
'Content-Type': 'application/json'
}
})
if(!register_response.ok){
throw new Error(`Register failed with ${login_response.status}`)
}
const user_data = await register_response.json();
return user_data;
}
}
\ No newline at end of file
import LoginView from "./login_view.js"
class LoginPageView extends EventTarget {
constructor(){
this.login_form = new LoginView(document.querySelector('login-form'))
this.login_form.addEventListener("login_attempt", (event) => {
this.dispatchEvent(new CustomEvent("login_attempt", {detail: {login: event.detail.login, password: event.detail.password}}))
})
this.register_form.addEventListener("register_attempt", (event) => {
this.dispatchEvent(new CustomEvent("register_attempt", {detail: {
login: event.detail.login,
password: event.detail.password,
confirm_password: event.detail.confirm_password}
}))
})
}
}
export default LoginPageView;
\ No newline at end of file
<nav-bar>
<bar-title>
<a href="/">Conquy Blade</a>
</bar-title>
<bar-options>
<div class="dropdown">
<p>Unit</p>
<div class="dropdown-content">
<a>All Units</a>
<a>My Units</a>
</div>
</div>
<div class="dropdown">
<p>House</p>
<div class="dropdown-content">
<a>All Units</a>
<a>My Units</a>
</div>
</div>
</bar-options>
<bar-user>
<a href="/login">Login/Register</a>
</bar-user>
</nav-bar>
<script src="navbar.js"></script>
\ No newline at end of file
function loadPage(href)
{
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", href, false);
xmlhttp.send();
return xmlhttp.responseText;
}
const nav_placeholder = document.querySelector('nav-placeholder')
nav_placeholder.innerHTML = loadPage('/navbar/navbar.html')
const bar_user = document.querySelector('bar-user a');
console.log(bar_user)
const saved_user = localStorage.getItem('username');
if(saved_user){
bar_user.innerText = saved_user;
}
function testy(){
console.log('asd')
}
\ No newline at end of file
This diff is collapsed.
content-body{
grid-area: content;
}
center-div{
text-align: center;
left: 50%;
position: absolute;
display: inline-block;
margin-top: 50px;
margin-left: -250px;
background-color: rgb(27, 26, 26);
height: 500px;
width: 500px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
}
.tab {
overflow: hidden;
// border: 1px solid #ccc;
background-color: #383838;
button{
font-family: monospace;
font-size: 20px;
}
}
/* Style the buttons that are used to open the tab content */
.tab button {
background-color: inherit;
// float: left;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
}
/* Change background color of buttons on hover */
.tab button:hover {
background-color: #ddd;
}
/* Create an active/current tablink class */
.tab button.active {
background-color: #ccc;
}
/* Style the tab content */
.tabcontent {
display: none;
padding: 6px 12px;
border: 1px solid #ccc;
border-top: none;
}
.tabcontent {
animation: fadeEffect 1s; /* Fading effect takes 1 second */
}
/* Go from zero to full opacity */
@keyframes fadeEffect {
from {opacity: 0;}
to {opacity: 1;}
}
\ No newline at end of file
@import "../fontawesome/font_awesome.scss";
@import "./navbar.scss";
@import "./login.scss";
body{
font-family: monospace;
background-color: rgb(49, 49, 49);
color: whitesmoke;
display: grid;
grid-template:
"navbar" 100px
"content" 1fr
"footer" 200px
}
\ No newline at end of file
nav-bar{
grid-area: navbar;
display: grid;
color: rgb(219, 219, 219);
grid-template:
"title options user"
/1fr 5fr 1fr
;
background-color: rgb(117, 16, 16);
}
bar-title {
margin: auto;
a {
grid-area: title;
font-size: 300%;
color: whitesmoke;
text-decoration: none;
}
}
bar-options{
grid-area: options;
}
.dropdown {
position: relative;
display: inline-block;
text-align: center;
font-size: 30px;
min-width: 160px;
height: 100%;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #2c2c2c;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
padding: 12px 16px;
z-index: 1;
a:hover{
background-color: rgb(59, 15, 15);
}
}
.dropdown:hover {
background-color: #9c4949;
.dropdown-content {
display: block;
}
}
bar-user{
font-size: 30px;
text-align: center;
padding-top: 30px;
a{
text-decoration: none;
color: whitesmoke;
}
}
bar-user:hover{
background-color: rgb(95, 26, 26);
}
\ No newline at end of file
content-body {
grid-area: content;
}
center-div {
text-align: center;
left: 50%;
position: absolute;
display: inline-block;
margin-top: 50px;
margin-left: -250px;
background-color: #1b1a1a;
height: 500px;
width: 500px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
}
.tab {
overflow: hidden;
background-color: #383838;
}
.tab button {
font-family: monospace;
font-size: 20px;
}
/* Style the buttons that are used to open the tab content */
.tab button {
background-color: inherit;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
}
/* Change background color of buttons on hover */
.tab button:hover {
background-color: #ddd;
}
/* Create an active/current tablink class */
.tab button.active {
background-color: #ccc;
}
/* Style the tab content */
.tabcontent {
display: none;
padding: 6px 12px;
border: 1px solid #ccc;
border-top: none;
}
.tabcontent {
animation: fadeEffect 1s;
/* Fading effect takes 1 second */
}
/* Go from zero to full opacity */
@keyframes fadeEffect {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/*# sourceMappingURL=login.css.map */
{"version":3,"sourceRoot":"","sources":["../sass/login.scss"],"names":[],"mappings":"AAAA;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EAEA;;AAEA;EACI;EACA;;;AAIN;AACF;EACI;EAEA;EACA;EACA;EACA;EACA;;;AAGF;AACF;EACI;;;AAGF;AACF;EACI;;;AAGF;AACF;EACI;EACA;EACA;EACA;;;AAGJ;EACI;AAA0B;;;AAG9B;AACA;EACI;IAAM;;EACN;IAAI","file":"login.css"}
\ No newline at end of file
@font-face {
font-family: "Font Awesome 5 Brands";
font-style: normal;
font-weight: 400;
font-display: block;
src: url("/fontawesome/fa-brands-400.woff2") format("woff2");
}
@font-face {
font-family: "Font Awesome 5 Free";
font-style: normal;
font-weight: 400;
font-display: block;
src: url("/fontawesome/fa-regular-400.woff2") format("woff2");
}
@font-face {
font-family: "Font Awesome 5 Free";
font-style: normal;
font-weight: 900;
font-display: block;
src: url("/fontawesome/fa-solid-900.woff2") format("woff2");
}
@keyframes fa-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
nav-bar {
grid-area: navbar;
display: grid;
color: #dbdbdb;
grid-template: "title options user"/1fr 5fr 1fr;
background-color: #751010;
}
bar-title {
margin: auto;
}
bar-title a {
grid-area: title;
font-size: 300%;
color: whitesmoke;
text-decoration: none;
}
bar-options {
grid-area: options;
}
.dropdown {
position: relative;
display: inline-block;
text-align: center;
font-size: 30px;
min-width: 160px;
height: 100%;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #2c2c2c;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
padding: 12px 16px;
z-index: 1;
}
.dropdown-content a:hover {
background-color: #3b0f0f;
}
.dropdown:hover {
background-color: #9c4949;
}
.dropdown:hover .dropdown-content {
display: block;
}
bar-user {
font-size: 30px;
text-align: center;
padding-top: 30px;
}
bar-user a {
text-decoration: none;
color: whitesmoke;
}
bar-user:hover {
background-color: #5f1a1a;
}
content-body {
grid-area: content;
}
center-div {
text-align: center;
left: 50%;
position: absolute;
display: inline-block;
margin-top: 50px;
margin-left: -250px;
background-color: #1b1a1a;
height: 500px;
width: 500px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
}
.tab {
overflow: hidden;
background-color: #383838;
}
.tab button {
font-family: monospace;
font-size: 20px;
}
/* Style the buttons that are used to open the tab content */
.tab button {
background-color: inherit;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
transition: 0.3s;
}
/* Change background color of buttons on hover */
.tab button:hover {
background-color: #ddd;
}
/* Create an active/current tablink class */
.tab button.active {
background-color: #ccc;
}
/* Style the tab content */
.tabcontent {
display: none;
padding: 6px 12px;
border: 1px solid #ccc;
border-top: none;
}
.tabcontent {
animation: fadeEffect 1s;
/* Fading effect takes 1 second */
}
/* Go from zero to full opacity */
@keyframes fadeEffect {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
body {
font-family: monospace;
background-color: #313131;
color: whitesmoke;
display: grid;
grid-template: "navbar" 100px "content" 1fr "footer" 200px;
}
/*# sourceMappingURL=main.css.map */
{"version":3,"sourceRoot":"","sources":["../fontawesome/font_awesome.scss","../sass/navbar.scss","../sass/login.scss","../sass/main.scss"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAmBF;EACI;IAAK;;EACP;IAAO;;;AC1CX;EACI;EACA;EACA;EACA,eACI;EAGJ;;;AAGJ;EAEI;;AACA;EACI;EACA;EAEA;EACA;;;AAIR;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACI;;;AAIR;EACI;;AACA;EACI;;;AAIR;EACI;EACA;EACA;;AACA;EACI;EACA;;;AAIR;EACI;;;ACnEJ;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EAEA;;AAEA;EACI;EACA;;;AAIN;AACF;EACI;EAEA;EACA;EACA;EACA;EACA;;;AAGF;AACF;EACI;;;AAGF;AACF;EACI;;;AAGF;AACF;EACI;EACA;EACA;EACA;;;AAGJ;EACI;AAA0B;;;AAG9B;AACA;EACI;IAAM;;EACN;IAAI;;;AC5DR;EACI;EACA;EACA;EACA;EACA,eACI","file":"main.css"}
\ No newline at end of file
nav-bar {
grid-area: navbar;
display: grid;
color: #dbdbdb;
grid-template: "title options user"/1fr 5fr 1fr;
background-color: #751010;
}
bar-title {
margin: auto;
}
bar-title a {
grid-area: title;
font-size: 300%;
color: whitesmoke;
text-decoration: none;
}
bar-options {
grid-area: options;
}
.dropdown {
position: relative;
display: inline-block;
text-align: center;
font-size: 30px;
min-width: 160px;
height: 100%;
}
.dropdown-content {
display: none;
position: absolute;
background-color: #2c2c2c;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
padding: 12px 16px;
z-index: 1;
}
.dropdown-content a:hover {
background-color: #3b0f0f;
}
.dropdown:hover {
background-color: #9c4949;
}
.dropdown:hover .dropdown-content {
display: block;
}
bar-user {
font-size: 30px;
text-align: center;
padding-top: 30px;
}
bar-user a {
text-decoration: none;
color: whitesmoke;
}
bar-user:hover {
background-color: #5f1a1a;
}
/*# sourceMappingURL=navbar.css.map */
{"version":3,"sourceRoot":"","sources":["../sass/navbar.scss"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;EACA,eACI;EAGJ;;;AAGJ;EAEI;;AACA;EACI;EACA;EAEA;EACA;;;AAIR;EACI;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACI;;;AAIR;EACI;;AACA;EACI;;;AAIR;EACI;EACA;EACA;;AACA;EACI;EACA;;;AAIR;EACI","file":"navbar.css"}
\ No newline at end of file
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment