diff --git a/backend/src/main/java/achievements/Application.java b/backend/src/main/java/achievements/Application.java index b622eff..971ab08 100644 --- a/backend/src/main/java/achievements/Application.java +++ b/backend/src/main/java/achievements/Application.java @@ -1,5 +1,6 @@ package achievements; +import achievements.misc.Password; import achievements.services.DbConnectionService; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/backend/src/main/java/achievements/controllers/Controller.java b/backend/src/main/java/achievements/controllers/Controller.java index 7595ba5..bd1a0d3 100644 --- a/backend/src/main/java/achievements/controllers/Controller.java +++ b/backend/src/main/java/achievements/controllers/Controller.java @@ -1,6 +1,7 @@ package achievements.controllers; import achievements.data.Achievements; +import achievements.data.User; import achievements.data.Games; import achievements.data.InternalError; import achievements.services.DbService; @@ -12,6 +13,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.POST; @RestController public class Controller { @@ -62,4 +64,47 @@ public class Controller { return new ResponseEntity("{}", HttpStatus.INTERNAL_SERVER_ERROR); } } + + /** + * Acceptable codes + * 0 => Success + * 1 => Email already registered + * + * -1 => Unknown error + */ + @RequestMapping(value = "/create_user", method = POST, consumes = "application/json", produces = "application/json") + public ResponseEntity createUser(@RequestBody User user) { + var status = db.createUser(user); + if (status == 0) { + return ResponseEntity.ok("{ \"key\": \"aoeuhtns\" }"); + //var sessionKey = db.generateSessionKey(user); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("{ \"code\": " + status + " }"); + } + } + + /** + * DO NOT RETURN CODE DIRECTLY! + * + * User should only ever recieve -1, 0, or 1. The specific authentication error should be hidden. + * + * Acceptable codes + * 0 => Success + * 1 => Unregistered email address + * 2 => Incorrect password + * + * -1 => Unknown error + */ + @RequestMapping(value = "/login", method = POST, consumes = "application/json", produces = "application/json") + public ResponseEntity login(@RequestBody User user) { + var status = db.login(user); + if (status == 0) { + return ResponseEntity.ok("{ \"key\": \"aoeuhtns\" }"); + } else if (status > 0) { + // Hardcoded 1 response code + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("{ \"code\": 1 }"); + } else { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("{ \"code\": " + status + " }"); + } + } } diff --git a/backend/src/main/java/achievements/data/User.java b/backend/src/main/java/achievements/data/User.java new file mode 100644 index 0000000..b0e086b --- /dev/null +++ b/backend/src/main/java/achievements/data/User.java @@ -0,0 +1,43 @@ +package achievements.data; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class User { + + @JsonProperty("email") + public String email; + @JsonProperty("username") + public String username; + @JsonProperty("password") + public String password; + + public User(String email, String username, String password) { + this.email = email; + this.username = username; + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/backend/src/main/java/achievements/misc/Password.java b/backend/src/main/java/achievements/misc/Password.java new file mode 100644 index 0000000..3707221 --- /dev/null +++ b/backend/src/main/java/achievements/misc/Password.java @@ -0,0 +1,95 @@ +package achievements.misc; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; + +public class Password { + + private static final Random RANDOM = new SecureRandom(); + + public final String salt; + public final String hash; + + private Password(String salt, String hash) { + this.salt = salt; + this.hash = hash; + } + + public static Password generate(String password) { + // Generate the salt + var salt = new byte[16]; // 128 bits + RANDOM.nextBytes(salt); + + return new Password( + encode(salt), + encode(hash(salt, password.getBytes())) + ); + } + + public static boolean validate(String salt, String password, String hash) { + System.out.println(salt + ", " + password); + var srcHash = hash(decode(salt), password.getBytes()); + var targetHash = decode(hash); + for (int i = 0; i < srcHash.length; ++i) { + if (srcHash[i] != targetHash[i]) { + return false; + } + } + return true; + } + + private static byte[] hash(byte[] salt, byte[] password) { + try { + var concat = new byte[salt.length + password.length]; + int i = 0; + for (; i < salt.length; ++i) { + concat[i] = salt[i]; + } + for (int j = 0; j < password.length; ++j) { + concat[i + j] = password[j]; + } + + var md = MessageDigest.getInstance("SHA-256"); + return md.digest(concat); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return null; + } + + private static String encode(byte[] bytes) { + var chars = new char[bytes.length << 1]; + for (int i = 0; i < bytes.length; ++i) { + chars[(i << 1) ] = toHex(bytes[i] >> 0); + chars[(i << 1) + 1] = toHex(bytes[i] >> 4); + } + return new String(chars); + } + + private static byte[] decode(String data) { + var decoded = new byte[data.length() >> 1]; + for (int i = 0; i < data.length(); i += 2) { + int currentByte = + (fromHex(data.charAt(i )) ) | + (fromHex(data.charAt(i + 1)) << 4); + decoded[i >> 1] = (byte) (currentByte & 0xFF); + } + return decoded; + } + + private static char toHex(int halfByte) { + halfByte = halfByte & 0xF; + if (0 <= halfByte && halfByte <= 9 ) return (char) (halfByte + '0' ); + if (10 <= halfByte && halfByte <= 15) return (char) (halfByte + 'a' - 10); + return '0'; + } + + private static int fromHex(char c) { + if ('0' <= c && c <= '9') return c - '0'; + if ('A' <= c && c <= 'F') return c - 'A' + 10; + if ('a' <= c && c <= 'f') return c - 'a' + 10; + return 0; + } +} diff --git a/backend/src/main/java/achievements/services/DbService.java b/backend/src/main/java/achievements/services/DbService.java index 20eca43..d00df35 100644 --- a/backend/src/main/java/achievements/services/DbService.java +++ b/backend/src/main/java/achievements/services/DbService.java @@ -2,6 +2,8 @@ package achievements.services; import achievements.data.Achievements; import achievements.data.Games; +import achievements.data.User; +import achievements.misc.Password; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -84,4 +86,49 @@ public class DbService { return null; } } + + public int createUser(User user) { + try { + var statement = db.prepareCall("{? = call CreateUser(?, ?, ?, ?)}"); + statement.registerOutParameter(1, Types.INTEGER); + statement.setString(2, user.getEmail()); + statement.setString(3, user.getUsername()); + + var password = Password.generate(user.getPassword()); + statement.setString(4, password.salt); + statement.setString(5, password.hash); + + statement.execute(); + var code = statement.getInt(1); + statement.close(); + + return code; + } catch (SQLException e) { + e.printStackTrace(); + } + return -1; + } + + public int login(User user) { + try { + var statement = db.prepareStatement("SELECT Salt, Password FROM [dbo].[User] WHERE Email = ?"); + statement.setString(1, user.email); + + var result = statement.executeQuery(); + if (result.next()) { + var salt = result.getString("Salt"); + var hash = result.getString("Password"); + if (Password.validate(salt, user.getPassword(), hash)) { + return 0; + } else { + return 2; + } + } else { + return 1; + } + } catch (SQLException e) { + e.printStackTrace(); + } + return -1; + } } diff --git a/frontend/static_server.js b/frontend/static_server.js index 75509c5..bcf5afe 100644 --- a/frontend/static_server.js +++ b/frontend/static_server.js @@ -1,7 +1,5 @@ -const express = require('express'); -const morgan = require('morgan' ); -const fs = require('fs' ); -const https = require('https' ); +const express = require('express'); +const morgan = require('morgan' ); const config = require('./config.js').load(process.argv[2]); diff --git a/frontend/webpage/index.html b/frontend/webpage/index.html index f5ccc0d..0ca9edc 100644 --- a/frontend/webpage/index.html +++ b/frontend/webpage/index.html @@ -4,6 +4,8 @@ Achievements Project + + diff --git a/frontend/webpage/login.html b/frontend/webpage/login.html new file mode 100644 index 0000000..64fd570 --- /dev/null +++ b/frontend/webpage/login.html @@ -0,0 +1,41 @@ + + + + + Achievements Project | Login + + + + + + + +
+
+ +
+

Login

+
+
+
+

Egg

+ + + + +
+ + +
+

WARNING! The security of this project is questionable at best. Please refrain from using any truly sensitive data.

+
+
+
+ + + + + \ No newline at end of file diff --git a/frontend/webpage/scripts/login.js b/frontend/webpage/scripts/login.js new file mode 100644 index 0000000..7096ca8 --- /dev/null +++ b/frontend/webpage/scripts/login.js @@ -0,0 +1,122 @@ +window.addEventListener("load", (loadEvent) => { + const fields = { + email: document.querySelector("#email" ), + username: document.querySelector("#username"), + password: document.querySelector("#password"), + confirm: document.querySelector("#confirm" ) + }; + + const createUser = document.querySelector("#create-user-button"); + const login = document.querySelector("#login-button"); + + const header = document.querySelector("#login-header-text"); + const error = document.querySelector("#error-message"); + + const raiseError = (errorFields, message) => { + for (const key in fields) { + if (errorFields.includes(key)) { + fields[key].classList.add("error"); + } else { + fields[key].classList.remove("error"); + } + } + + error.style.display = "block"; + error.textContent = message; + } + + let frozen = false; + + const switchToCreateAction = (clickEvent) => { + if (!frozen) { + fields.username.style.display = "block"; + fields.confirm.style.display = "block"; + login.style.display = "none"; + header.textContent = "Create User"; + + createUser.removeEventListener("click", switchToCreateAction); + createUser.addEventListener("click", createUserAction); + } + }; + const createUserAction = (clickEvent) => { + if (!frozen) { + if (fields.email.value === '') { + raiseError([ "email" ], "Email cannot be empty"); + } else if (fields.username.value === '') { + raiseError([ "username" ], "Username cannot be empty"); + } else if (fields.password.value !== fields.confirm.value) { + raiseError([ "password", "confirm" ], "Password fields did not match"); + } else if (fields.password.value === '') { + raiseError([ "password", "confirm" ], "Password cannot be empty"); + } else { + frozen = true; + fetch('https://localhost:4730/create_user', { + method: 'POST', + mode: 'cors', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ email: fields.email.value, username: fields.username.value, password: fields.password.value }) + }) + .then(async response => ({ status: response.status, data: await response.json() })) + .then(response => { + const data = response.data; + if (response.status === 200) { + window.sessionStorage.setItem('sessionKey', data.key); + window.location.href = "/"; + } else if (response.status === 500) { + raiseError([], "Internal server error :("); + } else { + if (data.code === 1) { + raiseError([ "email" ], "A user with that email is already registered"); + fields.email.value = ''; + } + } + }) + .catch(error => { + console.error(error); + raiseError([], "Server error :("); + }).then(() => frozen = false); + } + } + }; + createUser.addEventListener("click", switchToCreateAction); + + const loginAction = (clickEvent) => { + if (!frozen) { + if (fields.email.value === '') { + raiseError([ "email" ], "Email cannot be empty"); + } else if (fields.password.value === '') { + raiseError([ "password" ], "Password cannot be empty"); + } else { + frozen = true; + fetch('https://localhost:4730/login', { + method: 'POST', + mode: 'cors', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ email: fields.email.value, password: fields.password.value }) + }) + .then(async response => ({ status: response.status, data: await response.json() })) + .then(response => { + const data = response.data; + if (response.status === 200) { + window.sessionStorage.setItem('sessionKey', data.key); + window.location.href = "/"; + } else if (response.status === 500) { + raiseError([], "Internal server error :("); + } else { + raiseError([ "email", "password" ], "Email or password is incorrect"); + fields.password.value = ''; + } + }) + .catch(error => { + console.error(error); + raiseError([], "Unknown error :("); + }).then(() => frozen = false); + } + } + }; + login.addEventListener("click", loginAction); +}); \ No newline at end of file diff --git a/frontend/webpage/scripts/template.js b/frontend/webpage/scripts/template.js index 7e302ea..ecc1a88 100644 --- a/frontend/webpage/scripts/template.js +++ b/frontend/webpage/scripts/template.js @@ -1,142 +1,141 @@ var template = template || {}; -template.type = {}; -template.type._entryMap = new Map(); -template.type.register = (type, callback) => { - if (typeof type !== 'string') { - console.error(`'type' must be a string, recieved: `, type); - } else { - const TYPE_REGEX = /^(\w+)\s*(<\s*\?(?:\s*,\s*\?)*\s*>)?\s*$/; - const result = type.match(TYPE_REGEX); - if (result === null) { - console.error(`'${type}' is not a valid type id`); - } else { - if (result[2] === undefined) { - result[2] = 0; - } else { - result[2] = result[2].split(/\s*,\s*/).length; - } - const completeType = result[1] + ':' + result[2]; - if (template.type._entryMap.get(completeType) === undefined) { - template.type._entryMap.set(completeType, async function() { - await callback.apply(null, Array.from(arguments)); - }); - } else { - console.error(`${type} is already a registered template!`); - } - } - } -}; - -// Courtesy of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|\[\]\\]/g, '\\$&'); // $& means the whole matched string -} - -/* Intrinsic Templates */ - -// Basic - Simple search and replace -template.type.register('Basic', (element, map) => { - let html = element.innerHTML; - function applyObject(object, path) { - for (const key in object) { - const regexKey = escapeRegExp(path + key); - html = html.replace(new RegExp(`(?:(? { - return fetch(`templates/${name}.html.template`, { - method: 'GET', - mode: 'no-cors', - headers: { - 'Content-Type': 'text/plain' - } - }).then(response => response.text().then((data) => { - element.outerHTML = data; - })).catch(error => { - console.error(`failed to retrieve template '${name}': `, error); - }); -}); - -// List - Iterate over list and emit copy of child for each iteration -template.type.register('List', async (element, subtype, arrayMap) => { - let cumulative = ''; - const temp = document.createElement('template'); - for (const obj of arrayMap) { - temp.innerHTML = ``; - const child = temp.content.children[0]; - child.innerHTML = element.innerHTML; - const callback = template.type._entryMap.get(subtype.type); - if (callback === undefined) { - cumulative = ''; - console.error(`'${subtype.type}' is not a registered template type`); - } else { - await callback.apply(null, [ child, obj ]); - } - cumulative = cumulative + temp.innerHTML.trim(); - } - element.outerHTML = cumulative; -}); - -template._entryMap = new Map(); -template.apply = function(pattern, promise) { - if (typeof pattern !== 'string') { - console.error('pattern must be a string, received: ', pattern); - } else { - return new template.apply.applicators(pattern); - } -}; -template.apply.applicators = class { - constructor(pattern) { - this._pattern = pattern; - } - - _apply(asyncArgs) { - template._entryMap.set(RegExp('^' + this._pattern + '$'), asyncArgs); - } - - values(...args) { - this._apply(async () => Array.from(args)); - } - - promise(promise) { - let args = null; - promise = promise.then(data => args = [ data ]); - this._apply(async () => args || promise); - } - - fetch(dataProcessor, url, options) { - if (typeof dataProcessor === 'string') { - const path = dataProcessor; - dataProcessor = data => { - for (const id of path.split(/\./)) { - data = data[id]; - if (data === undefined) { - throw `invalid path '${path}'`; - } - } - - return data; - }; - }; - this.promise( - fetch(url, options || { method: 'GET', mode: 'cors' }) - .then(response => response.json()) - .then(data => dataProcessor(data)) - ); - } -}; - (() => { + templateTypeEntryMap = new Map(); + template.register = (type, callback) => { + if (typeof type !== 'string') { + console.error(`'type' must be a string, recieved: `, type); + } else { + const TYPE_REGEX = /^(\w+)\s*(<\s*\?(?:\s*,\s*\?)*\s*>)?\s*$/; + const result = type.match(TYPE_REGEX); + if (result === null) { + console.error(`'${type}' is not a valid type id`); + } else { + if (result[2] === undefined) { + result[2] = 0; + } else { + result[2] = result[2].split(/\s*,\s*/).length; + } + const completeType = result[1] + ':' + result[2]; + if (templateTypeEntryMap.get(completeType) === undefined) { + templateTypeEntryMap.set(completeType, async function() { + await callback.apply(null, Array.from(arguments)); + }); + } else { + console.error(`${type} is already a registered template!`); + } + } + } + }; + + // Courtesy of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|\[\]\\]/g, '\\$&'); // $& means the whole matched string + } + + /* Intrinsic Templates */ + + // Basic - Simple search and replace + template.register('Basic', (element, map) => { + let html = element.innerHTML; + function applyObject(object, path) { + for (const key in object) { + const regexKey = escapeRegExp(path + key); + html = html.replace(new RegExp(`(?:(? { + return fetch(`templates/${name}.html.template`, { + method: 'GET', + mode: 'no-cors', + headers: { + 'Content-Type': 'text/plain' + } + }).then(response => response.text().then((data) => { + element.outerHTML = data; + })).catch(error => { + console.error(`failed to retrieve template '${name}': `, error); + }); + }); + + // List - Iterate over list and emit copy of child for each iteration + template.register('List', async (element, subtype, arrayMap) => { + let cumulative = ''; + const temp = document.createElement('template'); + for (const obj of arrayMap) { + temp.innerHTML = ``; + const child = temp.content.children[0]; + child.innerHTML = element.innerHTML; + const callback = templateTypeEntryMap.get(subtype.type); + if (callback === undefined) { + cumulative = ''; + console.error(`'${subtype.type}' is not a registered template`); + } else { + await callback.apply(null, [ child, obj ]); + } + cumulative = cumulative + temp.innerHTML.trim(); + } + element.outerHTML = cumulative; + }); + + templateEntryMap = new Map(); + template.apply = function(pattern, promise) { + if (typeof pattern !== 'string') { + console.error('pattern must be a string, received: ', pattern); + } else { + return new template.apply.applicators(pattern); + } + }; + template.apply.applicators = class { + constructor(pattern) { + this._pattern = pattern; + } + + _apply(asyncArgs) { + templateEntryMap.set(RegExp('^' + this._pattern + '$'), asyncArgs); + } + + values(...args) { + this._apply(async () => Array.from(args)); + } + + promise(promise) { + let args = null; + promise = promise.then(data => args = [ data ]); + this._apply(async () => args || promise); + } + + fetch(dataProcessor, url, options) { + if (typeof dataProcessor === 'string') { + const path = dataProcessor; + dataProcessor = data => { + for (const id of path.split(/\./)) { + data = data[id]; + if (data === undefined) { + throw `invalid path '${path}'`; + } + } + + return data; + }; + }; + this.promise( + fetch(url, options || { method: 'GET', mode: 'cors' }) + .then(response => response.json()) + .then(data => dataProcessor(data)) + ); + } + }; + const parseType = (type) => { let result = type.match(/^\s*(\w+)\s*(?:<(.*)>)?\s*$/); let id = result[1]; @@ -162,12 +161,12 @@ template.apply.applicators = class { let promises = []; let parents = new Set(); for (const child of children) { - for (const [pattern, argsCallback] of template._entryMap) { + for (const [pattern, argsCallback] of templateEntryMap) { await argsCallback().then(args => { if (pattern.test(child.id)) { - const callback = template.type._entryMap.get(child.typeCapture.type); + const callback = templateTypeEntryMap.get(child.typeCapture.type); if (typeof callback !== 'function') { - console.error(`'${child.typeCapture.type}' is not a registered template type`); + console.error(`'${child.typeCapture.type}' is not a registered template`); } else { let params = Array.from(args) for (const subtype of child.typeCapture.params) { diff --git a/frontend/webpage/styles/common.css b/frontend/webpage/styles/common.css new file mode 100644 index 0000000..6fe8b0d --- /dev/null +++ b/frontend/webpage/styles/common.css @@ -0,0 +1,378 @@ +html, body { + background-color: var(--background-dark); + + margin: 0; + border: 0; + padding: 0; + width: 100%; + height: 100%; + + font-family: sans-serif; +} + +#navbar { + z-index: 1; + + position: fixed; + + background-color: var(--accent-value2); + color: var(--foreground); + + width: 100%; + min-height: 76px; + height: 5%; + + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + box-shadow: 0px 0px 5px 10px rgba(0, 0, 0, 0.5); +} + +.navbar-section { + width: max-content; + height: 100%; + + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.navbar-item { + box-sizing: border-box; + padding: 0px 20px; + + width: max-content; + height: 100%; + + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + font-size: 24px; + + user-select: none; + + transition-property: background-color; + transition-duration: 0.15s; + + position: relative; +} + +.navbar-item:hover { + background-color: var(--accent-value3); +} + +.ap-button { + box-sizing: border-box; + + padding: 12px 16px; + + color: var(--foreground); + background-color: var(--accent-value2); + + font-size: 18px; + text-align: center; + + border-radius: 4px; + + cursor: default; + + transition-property: background-color; + transition-duration: 0.15s; +} + +.ap-button:hover { + background-color: var(--accent-value3); +} + +.ap-button:active { + background-color: var(--accent-value1); +} + +#content-body { + position: relative; + + top: max(76px, 5%); + + width: 100%; + height: calc(100% - max(76px, 5%)); + + overflow-y: auto; + + display: flex; + justify-content: center; +} + +.page { + z-index: 0; + + box-sizing: border-box; + + padding: 0px 64px; + + width: 100%; + height: max-content; + min-height: 100%; + + background-color: var(--background); + box-shadow: 0px 0px 5px 10px rgba(0, 0, 0, 0.5); + + display: none; +} + +.page-header { + box-sizing: border-box; + + padding: 64px 0px; + + width: 100%; + height: max-content; +} + +.page-header-text, +.page-subheader-text { + width: max-content; + + margin: 0; + margin-bottom: 0.25em; + + color: var(--foreground); + + cursor: default; +} + +.page-header-text.link, +.page-subheader-text.link { + transition-property: color; + transition-duration: 0.15s; +} + +.page-header-text.link:hover, +.page-subheader-text.link:hover { + color: var(--accent-value4); +} + +.page-header-text { + font-size: 64px; +} + +.page-subheader-text { + font-size: 48px; +} + +.page-header-separator, +.page-subheader-separator { + width: 100%; + height: 3px; + + background-color: var(--accent-value3); +} + +.list-page-search { + box-sizing: border-box; + + padding: 0px 64px 32px; + + width: 100%; + + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.list-page-search > label, +.list-page-search > input { + box-sizing: border-box; + padding: 12px 20px; + + color: var(--foreground); + + font-size: 24px; +} + +.list-page-search > label { + background-color: var(--accent-value2); + + border-radius: 8px 0px 0px 8px; +} + +.list-page-search > label:hover { + background-color: var(--accent-value3); +} + +.list-page-search > label:active { + background-color: var(--accent-value1); + + transition-property: background-color; + transition-duration: 0.15s; +} + +.list-page-search > input { + background-color: var(--distinction); + + border: 0; + border-radius: 0px 8px 8px 0px; + + flex-grow: 1; + + outline: none; + + transition-property: background-color, color; + transition-duration: 0.075s; +} + +.list-page-search > input:focus { + background-color: var(--foreground); + + color: var(--background); +} + +.list-page-partitions { + box-sizing: border-box; + + padding: 32px 64px; + + width: 100%; + height: max-content; + + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; +} + +.list-page-filter-partition { + width: 20%; + max-width: 640px; +} + +.list-page-filter { + margin-top: 16px; + + box-sizing: border-box; + + width: 100%; + height: max-content; + + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; +} + +.list-page-filter-checkbox { + width: 32px; + height: 32px; + + background-color: var(--background); + + border: 3px solid var(--distinction); + border-radius: 8px; + + transition-property: background-color, border-color; + transition-duration: 0.15s; +} + +.list-page-filter:hover > .list-page-filter-checkbox { + background-color: var(--background); + border-color: var(--selected-accent1); +} + +.list-page-filter.selected > .list-page-filter-checkbox { + background-color: var(--selected-accent1); + border-color: var(--selected-accent1); +} + +.list-page-filter.selected:hover > .list-page-filter-checkbox { + background-color: var(--selected-accent0); + border-color: var(--selected-accent1); +} + +.list-page-filter-name { + margin: 0; + padding: 16px; + + color: var(--foreground); + + font-size: 24px; + + user-select: none; +} + +.list-page-list-partition { + box-sizing: border-box; + + padding-left: 64px; + + flex-grow: 1; +} + +.list-page-list { + border-radius: 8px; + + overflow: hidden; +} + +.list-page-header { + width: 100%; + height: 64px; + + background-color: var(--accent-value2); + + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + color: var(--foreground); + font-size: 24px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.list-page-entry { + width: 100%; + height: 64px; + + background-color: var(--distinction); + + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + color: var(--foreground); + font-size: 24px; +} + +.list-page-entry-icon { + width: 64px; + height: 64px; + + flex-grow: 0; +} + +.list-page-entry-text { + box-sizing: border-box; + + margin: 0; + padding: 0 12px; + width: 0; + height: 64px; + line-height: 64px; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + border-top: 1px solid var(--background); +} + +.list-page-header > .list-page-entry-text { + border: 0; +} diff --git a/frontend/webpage/styles/index.css b/frontend/webpage/styles/index.css index a9fe898..5975fc8 100644 --- a/frontend/webpage/styles/index.css +++ b/frontend/webpage/styles/index.css @@ -1,25 +1,3 @@ -:root { - --background-dark: #111115; - --background: #22222A; - --foreground: #EEEEEE; - --distinction: #44444F; - - --accent-value0: #500000; - --accent-value1: #800000; - --accent-value2: #A00000; - --accent-value3: #D02020; - --accent-value4: #FA7575; - - --selected-accent0: #0066CC; - --selected-accent1: #3388FF; - - --navbar-background: var(--accent-value2); - --navbar-hover-background: var(--accent-value3); - --navbar-foreground: #EEEEEE; - - --header-color: var(--accent-value3); -} - html, body { background-color: var(--background-dark); @@ -32,394 +10,27 @@ html, body { font-family: sans-serif; } -#navbar { - z-index: 1; - - position: fixed; - - background-color: var(--navbar-background); - color: var(--navbar-foreground); - - width: 100%; - min-height: 76px; - height: 5%; - - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - - box-shadow: 0px 0px 5px 10px rgba(0, 0, 0, 0.5); -} - -.navbar-section { - width: max-content; - height: 100%; - - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; -} - -.navbar-item { - box-sizing: border-box; - padding: 0px 20px; - - width: max-content; - height: 100%; - - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - - font-size: 24px; - - user-select: none; - - transition-property: background-color; - transition-duration: 0.15s; - - position: relative; -} - -.navbar-item:hover { - background-color: var(--navbar-hover-background); -} - -#content-body { - position: relative; - - top: max(76px, 5%); - - width: 100%; - height: calc(100% - max(76px, 5%)); - - overflow-y: auto; - - display: flex; - justify-content: center; -} - -.page { - z-index: 0; - - box-sizing: border-box; - - padding: 0px 64px; - - width: 100%; - height: 100%; - - background-color: var(--background); - box-shadow: 0px 0px 5px 10px rgba(0, 0, 0, 0.5); - - overflow-y: auto; - - display: none; -} - -.page-header { - box-sizing: border-box; - - padding: 64px 0px; - - width: 100%; - height: max-content; -} - -.page-header-text { - width: max-content; - - margin: 0; - margin-bottom: 0.25em; - - color: var(--header-color); - - font-size: 64px; -} - -.page-header-separator { - width: 100%; - height: 3px; - - background-color: var(--foreground); -} - -.page-subheader-text { - width: max-content; - - margin: 0; - margin-bottom: 0.25em; - - color: var(--header-color); - - font-size: 48px; - - user-select: none; -} - -.page-subheader-separator { - width: 100%; - height: 3px; - - background-color: var(--foreground); - - transition-property: color; - transition-duration: 0.15s; -} - -.list-page-search { - box-sizing: border-box; - - padding: 32px 64px; - - width: 100%; - - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; -} - -.list-page-search > label { - box-sizing: border-box; - padding: 16px 24px; - - background-color: var(--accent-value2); - - color: var(--foreground); - - font-size: 32px; - - border-radius: 8px 0px 0px 8px; - - transition-property: background-color; - transition-duration: 0.15s; -} - -.list-page-search > label:hover { - background-color: var(--accent-value3); -} - -.list-page-search > label:active { - background-color: var(--accent-value1); -} - -.list-page-search > input { - box-sizing: border-box; - padding: 16px 24px; - - background-color: var(--distinction); - - color: var(--foreground); - - font-size: 32px; - - border: 0; - border-radius: 0px 8px 8px 0px; - - flex-grow: 1; - - outline: none; - - transition-property: background-color, color; - transition-duration: 0.075s; -} - -.list-page-search > input:focus { - background-color: var(--foreground); - color: var(--background); -} - -.list-page-partitions { - box-sizing: border-box; - - padding: 32px 64px; - - width: 100%; - height: max-content; - - display: flex; - flex-direction: row; - justify-content: center; - align-items: flex-start; -} - -.list-page-filter-partition { - width: 20%; - max-width: 640px; -} - -.list-page-filter { - margin-top: 16px; - - box-sizing: border-box; - - width: 100%; - height: max-content; - - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; -} - -.list-page-filter-checkbox { - width: 32px; - height: 32px; - - background-color: var(--background); - - border: 3px solid var(--distinction); - border-radius: 8px; - - transition-property: background-color, border-color; - transition-duration: 0.15s; -} - -.list-page-filter:hover > .list-page-filter-checkbox { - background-color: var(--background); - border-color: var(--selected-accent1); -} - -.list-page-filter.selected > .list-page-filter-checkbox { - background-color: var(--selected-accent1); - border-color: var(--selected-accent1); -} - -.list-page-filter.selected:hover > .list-page-filter-checkbox { - background-color: var(--selected-accent0); - border-color: var(--selected-accent1); -} - -.list-page-filter-name { - margin: 0; - padding: 16px; - - color: var(--foreground); - - font-size: 24px; - - user-select: none; -} - -.list-page-list-partition { - box-sizing: border-box; - - padding-left: 64px; - - flex-grow: 1; -} - -.list-page-list { - border-radius: 8px; - - overflow: hidden; - overflow-y: auto; -} - -.list-page-list-header { - width: 100%; - height: 64px; - - background-color: var(--accent-value2); - - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - - color: var(--foreground); - font-size: 24px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.list-page-list-entry { - width: 100%; - height: 64px; - - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - - color: var(--foreground); - font-size: 24px; - - border-bottom: 1px solid var(--distinction); -} - #games-page { max-width: 1920px; } -.game-list-page-entry-icon { - width: 64px; - height: 64px; - - flex-grow: 0; -} - -.game-list-page-entry-name { - box-sizing: border-box; - - margin: 0; - padding: 0 12px; - width: 0; - - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - +.list-page-entry-text.game-name { flex-grow: 1; - flex-basis: 0px; } -.game-list-page-entry-description { - box-sizing: border-box; - - margin: 0; - padding: 0 12px; - width: 0; - - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - +.list-page-entry-text.game-description { flex-grow: 2; - flex-basis: 0px; } #achievements-page { max-width: 1920px; } -.achievement-list-page-entry-icon { - width: 64px; - height: 64px; - - flex-grow: 0; -} - -.achievement-list-page-entry-name { - box-sizing: border-box; - - margin: 0; - padding: 0 12px; - width: 0; - - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - +.list-page-entry-text.achievement-name { flex-grow: 4; - flex-basis: 0px; } -.achievement-list-page-entry-description { +.list-page-entry-text.achievement-description { box-sizing: border-box; margin: 0; @@ -431,22 +42,10 @@ html, body { white-space: nowrap; flex-grow: 8; - flex-basis: 0px; } -.achievement-list-page-entry-stages { - box-sizing: border-box; - - margin: 0; - padding: 0 12px; - width: 0; - - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - +.list-page-entry-text.achievement-stages { flex-grow: 1; - flex-basis: 0px; } #profile-page { @@ -482,6 +81,8 @@ html, body { #profile-info-pfp { width: 100%; max-width: 640px; + + margin-bottom: 1.25em; } #profile-info-name { @@ -491,7 +92,78 @@ html, body { font-size: 42px; - color: var(--header-color); + color: var(--foreground); +} + +.profile-list { + margin-top: 1.25em; + + width: 100%; + height: max-content; + + border-radius: 8px; + + overflow: hidden; +} + +.profile-entry { + overflow: hidden; + + height: 64px; + + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + background-color: var(--distinction); + + border-bottom: 1px solid var(--background); +} + +.profile-entry.accented { + border-bottom: 1px solid var(--accent-value0); + + background-color: var(--accent-value1); +} + +.profile-entry-left { + height: 100%; + + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; +} + +.profile-entry-right { + height: 100%; + + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; +} + +.profile-entry-icon { + width: 64px; +} + +.profile-entry-text { + margin: 0; + padding: 0px 16px; + + color: var(--foreground); + + font-size: 24px; +} + +.profile-entry-text.accented { + display: none; +} + +.profile-entry.accented .profile-entry-text.accented { + display: block; } #profile-platforms { @@ -504,74 +176,6 @@ html, body { flex-grow: 1; } -.profile-platform-entry { - overflow: hidden; - - margin-top: 16px; - - height: 64px; - - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - - border: 3px solid var(--distinction); - border-radius: 12px; -} - -.profile-platform-entry.connected { - border: 3px solid var(--accent-value3); - - background-color: var(--accent-value2); -} - -.profile-platform-entry-left { - height: 100%; - - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; -} - -.profile-platform-entry-right { - height: 100%; - - display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: center; -} - -.profile-platform-icon { - width: 64px; -} - -.profile-platform-name { - margin: 0; - padding: 0px 16px; - - color: var(--foreground); - - font-size: 24px; -} - -.profile-platform-connected { - display: none; - - margin: 0; - padding: 0px 16px; - - color: var(--foreground); - - font-size: 24px; -} - -.profile-platform-entry.connected .profile-platform-connected { - display: block; -} - #profile-section-2 { box-sizing: border-box; @@ -586,122 +190,16 @@ html, body { #profile-games { box-sizing: border-box; - padding: 0px 64px; + padding: 0px 64px 64px; width: 50%; height: max-content; } -#profile-games > .page-subheader-text:hover { - color: var(--accent-value4); -} - -.profile-game-entry { - overflow: hidden; - - margin-top: 16px; - - height: 64px; - - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - - border: 3px solid var(--distinction); - border-radius: 12px; -} - -.profile-game-entry-icon { - height: 64px; -} - -.profile-game-entry-name { - margin: 0; - padding: 0px 16px; - - color: var(--foreground); - - font-size: 24px; -} - #profile-achievements { box-sizing: border-box; - padding: 0px 64px; + padding: 0px 64px 64px; width: 50%; height: max-content; -} - -#profile-achievements > .page-subheader-text:hover { - color: var(--accent-value4); -} - -.profile-achievement-entry { - overflow: hidden; - - margin-top: 16px; - - height: 64px; - - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - - border: 3px solid var(--distinction); - border-radius: 12px; -} - -.profile-achievement-entry.completed { - border: 3px solid var(--accent-value3); - - background-color: var(--accent-value2); -} - -.profile-achievement-entry-left { - height: 100%; - - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; -} - -.profile-achievement-entry-right { - height: 100%; - - display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: center; -} - -.profile-achievement-entry-icon { - height: 64px; -} - -.profile-achievement-entry-name { - margin: 0; - padding: 0px 16px; - - color: var(--foreground); - - font-size: 24px; -} - -.profile-achievement-completed { - display: none; - - margin: 0; - padding: 0px 16px; - - color: var(--foreground); - - font-size: 24px; -} - -.profile-achievement-entry.completed .profile-achievement-completed { - display: block; -} - +} \ No newline at end of file diff --git a/frontend/webpage/styles/login.css b/frontend/webpage/styles/login.css new file mode 100644 index 0000000..707bc3a --- /dev/null +++ b/frontend/webpage/styles/login.css @@ -0,0 +1,101 @@ +:root { + --form-spacing: 48px; + + --element-spacing: 12px; + + --error: #FA7575; +} + +#login-page { + display: block; + + max-width: 1280px; +} + +#login-header { + box-sizing: border-box; + + padding: 0 calc(25% - 64px); + + width: 100%; + height: max-content; +} + +#login-form { + box-sizing: border-box; + + margin: 24px calc(25% - 64px) 0; + padding: 24px 0; + + height: max-content; + + background-color: var(--distinction); + + border-radius: 8px; +} + +#error-message { + display: none; + + box-sizing: border-box; + + margin: 0 var(--form-spacing) var(--element-spacing); + width: calc(100% - (var(--form-spacing) * 2)); + + color: var(--error); + + font-size: 20px; +} + +.login-field { + box-sizing: border-box; + + margin: 0 var(--form-spacing) var(--element-spacing); + border: 0; + padding: 8px; + + width: calc(100% - (var(--form-spacing) * 2)); + height: max-content; + + font-size: 20px; + + border-radius: 4px; + + outline: none; +} + +.login-field.error { + background-color: var(--error); +} + +#username, +#confirm { + display: none; +} + +#login-buttons { + margin: 0 calc(var(--form-spacing) - (var(--element-spacing) / 2)); + width: calc(100% - (var(--form-spacing) * 2) + var(--element-spacing)); + height: max-content; + + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} + +.ap-button.login { + margin: 0 calc(var(--element-spacing) / 2); + flex-grow: 1; + flex-basis: 0px; +} + +#warning { + box-sizing: border-box; + + padding: 0 var(--form-spacing); + + color: var(--foreground); + + font-size: 24px; +} \ No newline at end of file diff --git a/frontend/webpage/styles/theme.css b/frontend/webpage/styles/theme.css new file mode 100644 index 0000000..b2f059d --- /dev/null +++ b/frontend/webpage/styles/theme.css @@ -0,0 +1,16 @@ +:root { + --background-dark: #111115; + --background: #22222A; + --foreground-dark: #AAAAAA; + --foreground: #EEEEEE; + --distinction: #44444F; + + --accent-value0: #500000; + --accent-value1: #800000; + --accent-value2: #A00000; + --accent-value3: #D02020; + --accent-value4: #FA7575; + + --selected-accent0: #0066CC; + --selected-accent1: #3388FF; +} \ No newline at end of file diff --git a/frontend/webpage/templates/achievements_page.html.template b/frontend/webpage/templates/achievements_page.html.template index b4fb2a8..147ef4d 100644 --- a/frontend/webpage/templates/achievements_page.html.template +++ b/frontend/webpage/templates/achievements_page.html.template @@ -1,38 +1,38 @@

Filters

-
+

From Games Owned

-
+

In Progress

-
+

Completed

-
-

-

Name

-

Description

-

Stages

+
+

+

Name

+

Description

+

Stages

diff --git a/frontend/webpage/templates/games_page.html.template b/frontend/webpage/templates/games_page.html.template index 1977414..ecab835 100644 --- a/frontend/webpage/templates/games_page.html.template +++ b/frontend/webpage/templates/games_page.html.template @@ -1,6 +1,6 @@
@@ -13,40 +13,40 @@
-
-

-

Name

-

Description

+
+

+

Name

+

Description

-
- Achievement Icon.png -

Latin

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

+
+ Achievement Icon.png +

Latin

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
- Achievement Icon.png -

Latin

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

+
+ Achievement Icon.png +

Latin

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
- Achievement Icon.png -

Latin

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

+
+ Achievement Icon.png +

Latin

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
- Achievement Icon.png -

Latin

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

+
+ Achievement Icon.png +

Latin

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
- Achievement Icon.png -

Latin

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

+
+ Achievement Icon.png +

Latin

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
- Achievement Icon.png -

Latin

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

+
+ Achievement Icon.png +

Latin

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

diff --git a/frontend/webpage/templates/profile_page.html.template b/frontend/webpage/templates/profile_page.html.template index d2ce6a5..78c0090 100644 --- a/frontend/webpage/templates/profile_page.html.template +++ b/frontend/webpage/templates/profile_page.html.template @@ -1,134 +1,149 @@
User's Profile Pictuer +

Jane Doe

Platforms

-
-
- Steam Logo -

Steam

+
+
+
+ Steam Logo +

Steam

+
+
+

Connected

+
-
-

Connected

+
+
+ Xbox Logo +

Xbox Live

+
+
+

Connected

+
-
-
-
- Xbox Logo -

Xbox Live

-
-
-

Connected

-
-
-
-
- PSN Logo -

PSN

-
-
-

Connected

+
+
+ PSN Logo +

PSN

+
+
+

Connected

+
-

Games

+
-
- Some Game Icon -

Latin

-
-
- Some Game Icon -

Latin

-
-
- Some Game Icon -

Latin

-
-
- Some Game Icon -

Latin

+
+
+
+ Some Game Icon +

Latin

+
+
+
+
+ Some Game Icon +

Latin

+
+
+
+
+ Some Game Icon +

Latin

+
+
+
+
+ Some Game Icon +

Latin

+
+
-

Achievements

+
-
-
- Some Achievement Icon -

Lorem Ipsum

+
+
+
+ Some Achievement Icon +

Lorem Ipsum

+
+
+

Completed

+
-
-

Completed

+
+
+ Some Achievement Icon +

Lorem Ipsum

+
+
+

Completed

+
-
-
-
- Some Achievement Icon -

Lorem Ipsum

+
+
+ Some Achievement Icon +

Lorem Ipsum

+
+
+

Completed

+
-
-

Completed

+
+
+ Some Achievement Icon +

Lorem Ipsum

+
+
+

Completed

+
-
-
-
- Some Achievement Icon -

Lorem Ipsum

+
+
+ Some Achievement Icon +

Lorem Ipsum

+
+
+

Completed

+
-
-

Completed

+
+
+ Some Achievement Icon +

Lorem Ipsum

+
+
+

Completed

+
-
-
-
- Some Achievement Icon -

Lorem Ipsum

+
+
+ Some Achievement Icon +

Lorem Ipsum

+
+
+

Completed

+
-
-

Completed

-
-
-
-
- Some Achievement Icon -

Lorem Ipsum

-
-
-

Completed

-
-
-
-
- Some Achievement Icon -

Lorem Ipsum

-
-
-

Completed

-
-
-
-
- Some Achievement Icon -

Lorem Ipsum

-
-
-

Completed

-
-
-
-
- Some Achievement Icon -

Lorem Ipsum

-
-
-

Completed

+
+
+ Some Achievement Icon +

Lorem Ipsum

+
+
+

Completed

+
diff --git a/sql/CreateTables.sql b/sql/CreateTables.sql index beb45bb..1f6cc9e 100644 --- a/sql/CreateTables.sql +++ b/sql/CreateTables.sql @@ -31,7 +31,8 @@ CREATE TABLE [User] ( ID INT IDENTITY(0, 1) NOT NULL, Email VARCHAR(254) NOT NULL, Username VARCHAR(32) NOT NULL, - [Password] CHAR(256) NOT NULL + [Password] CHAR(64) NOT NULL, + [Salt] CHAR(32) NOT NULL PRIMARY KEY(ID) ) diff --git a/sql/CreateUserSP.sql b/sql/CreateUserSP.sql index 0af4edf..6a7f0d7 100644 Binary files a/sql/CreateUserSP.sql and b/sql/CreateUserSP.sql differ