Senin 18 November 2019
This commit is contained in:
parent
4d29a48f79
commit
7803ea548d
6
.htaccess
Normal file
6
.htaccess
Normal file
@ -0,0 +1,6 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*)$ /index.php/$1 [L]
|
||||
</IfModule>
|
133
Application/Controllers/AuthController.php
Normal file
133
Application/Controllers/AuthController.php
Normal file
@ -0,0 +1,133 @@
|
||||
<?php
|
||||
namespace Application\Controllers;
|
||||
|
||||
use Application\HTTP\Request;
|
||||
use Application\HTTP\Response;
|
||||
use Application\Services\ServiceContainer;
|
||||
use Application\Foundations\QueryBuilder;
|
||||
use Application\Foundations\MailBuilder;
|
||||
use Application\Models\User;
|
||||
use Application\Models\UserConfirmation;
|
||||
|
||||
class AuthController {
|
||||
public function __construct() {
|
||||
|
||||
}
|
||||
public function sign_up(Request $request, Response $response) {
|
||||
if(ServiceContainer::Authentication()->isLoggedIn()) {
|
||||
return $response->redirect('/');
|
||||
}
|
||||
return $response->view('sign-up');
|
||||
}
|
||||
public function create_user(Request $request, Response $response) {
|
||||
if($request->email == "") {
|
||||
return $response->redirect("/signup")->with(
|
||||
[ 'errors' => 'Email must not be empty' ]
|
||||
);
|
||||
} else if (!filter_var($request->email,FILTER_VALIDATE_EMAIL)) {
|
||||
return $response->redirect("/signup")->with(
|
||||
[ 'errors' => 'Email must not valid' ]
|
||||
);
|
||||
} else if ($request->username == "") {
|
||||
return $response->redirect("/signup")->with(
|
||||
[ 'errors' => 'Username must not be empty' ]
|
||||
);
|
||||
} else if (strlen($request->username) < 6 || strlen($request->username) > 20 ) {
|
||||
return $response->redirect("/signup")->with(
|
||||
[ 'errors' => 'Username must be between 6 and 20 characters' ]
|
||||
);
|
||||
} else if (strlen($request->password) < 8) {
|
||||
return $response->redirect("/signup")->with(
|
||||
[ 'errors' => 'Password must be at least 8 characters' ]
|
||||
);
|
||||
} else if ($request->password != $request->confirmpassword) {
|
||||
return $response->redirect("/signup")->with(
|
||||
[ 'errors' => 'Password and confirm password must be the same' ]
|
||||
);
|
||||
}
|
||||
ServiceContainer::Session()->unset('errors');
|
||||
$data = User::create([
|
||||
'id' => null,
|
||||
'username' => $request->username,
|
||||
'about' => '',
|
||||
'email' => $request->email,
|
||||
'email_visible' => 0,
|
||||
'avatar_path' => '',
|
||||
'password' => password_hash($request->password,PASSWORD_DEFAULT),
|
||||
'is_confirmed' => 0,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
if($data != null) {
|
||||
$id = $data->id;
|
||||
$confirmator = UserConfirmation::create([
|
||||
'confirm_key' => hash('sha256',$data->username.time()),
|
||||
'user_id' => $id,
|
||||
'best_before' => date('Y-m-d H:i:s', strtotime('+6 hours', time())),
|
||||
]);
|
||||
$email = new MailBuilder();
|
||||
$body = "Thank you for registering with metaforums.\n";
|
||||
$body .= "To be able to explore the vast forum, use the URL below:\n\n";
|
||||
$body .= $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['SERVER_NAME'].'/signup/confirm?'.$confirmator->confirm_key;
|
||||
$email->from("metaforums@nanao.moe")->to($data->email)->subject("Complete your registration on Metaforums")->body($body);
|
||||
ServiceContainer::Email()->send($email);
|
||||
if($confirmator != null) {
|
||||
return $response->redirect('/signup/success')->with(
|
||||
[ 'signup-email' => $request->email ],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
public function sign_up_success(Request $request, Response $response) {
|
||||
return $response->view('sign-up-success');
|
||||
}
|
||||
public function sign_up_confirm(Request $request, Response $response) {
|
||||
$confirm = UserConfirmation::find($request->queryString());
|
||||
if(isset($confirm)) {
|
||||
$id = $confirm->user_id;
|
||||
$user = User::find($id);
|
||||
$user->update([ 'is_confirmed' => 1 ]);
|
||||
$confirm->delete();
|
||||
}
|
||||
return $response->redirect('/');
|
||||
}
|
||||
public function login(Request $request, Response $response) {
|
||||
if(ServiceContainer::Authentication()->isLoggedIn()) {
|
||||
return $response->redirect('/');
|
||||
}
|
||||
return $response->view('login');
|
||||
}
|
||||
public function login_check(Request $request, Response $response) {
|
||||
if ($request->username == "") {
|
||||
return $response->redirect("/login")->with(
|
||||
[ 'errors' => 'Username must not be empty' ]
|
||||
);
|
||||
} else if ($request->password == "") {
|
||||
return $response->redirect("/login")->with(
|
||||
[ 'errors' => 'Password must not be empty' ]
|
||||
);
|
||||
}
|
||||
$query = new QueryBuilder();
|
||||
$query = $query->select('id,password')->from('user')->where('username',$request->username)->where('is_confirmed',1)->build();
|
||||
$result = ServiceContainer::Database()->select($query);
|
||||
if(count($result) == 0) {
|
||||
return $response->redirect("/login")->with(
|
||||
[ 'errors' => 'Wrong username or password' ]
|
||||
);
|
||||
} else {
|
||||
$password = $result[0]["password"];
|
||||
$verify = password_verify($request->password,$password);
|
||||
if(!$verify) {
|
||||
return $response->redirect("/login")->with(
|
||||
[ 'errors' => 'Wrong username or password' ]
|
||||
);
|
||||
}
|
||||
}
|
||||
ServiceContainer::Session()->set('user_id',$result[0]['id']);
|
||||
return $response->redirect('/');
|
||||
}
|
||||
public function logout(Request $request, Response $response) {
|
||||
ServiceContainer::Session()->destroy();
|
||||
return $response->redirect('/login');
|
||||
}
|
||||
}
|
15
Application/Controllers/IndexController.php
Normal file
15
Application/Controllers/IndexController.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace Application\Controllers;
|
||||
|
||||
use Application\HTTP\Request;
|
||||
use Application\HTTP\Response;
|
||||
use Application\Services\ServiceContainer;
|
||||
|
||||
class IndexController {
|
||||
public function __construct() {
|
||||
|
||||
}
|
||||
public function index(Request $request, Response $response) {
|
||||
return $response->view('index');
|
||||
}
|
||||
}
|
26
Application/Foundations/MailBuilder.php
Normal file
26
Application/Foundations/MailBuilder.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace Application\Foundations;
|
||||
|
||||
class MailBuilder {
|
||||
public $to = "";
|
||||
public $subject = "";
|
||||
public $message = "";
|
||||
public $headers = "";
|
||||
|
||||
public function to($to) {
|
||||
$this->to = $to;
|
||||
return $this;
|
||||
}
|
||||
public function subject($subject) {
|
||||
$this->subject = $subject;
|
||||
return $this;
|
||||
}
|
||||
public function body($body) {
|
||||
$this->message = $body;
|
||||
return $this;
|
||||
}
|
||||
public function from($from) {
|
||||
$this->headers .= "From: ".$from."\r\n";
|
||||
return $this;
|
||||
}
|
||||
}
|
71
Application/Foundations/Model.php
Normal file
71
Application/Foundations/Model.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
namespace Application\Foundations;
|
||||
|
||||
use Application\Services\ServiceContainer;
|
||||
|
||||
class Model {
|
||||
public $attributes;
|
||||
protected $primary_key = 'id';
|
||||
|
||||
public function __construct() {
|
||||
}
|
||||
public function hydrate($data) {
|
||||
$this->attributes = $data;
|
||||
}
|
||||
public static function create($data) {
|
||||
$calling_class = get_called_class();
|
||||
$class = explode('\\',get_called_class());
|
||||
$tablename = strtolower($class[count($class)-1]);
|
||||
$result = ServiceContainer::Database()->insert($tablename, $data);
|
||||
if($result) {
|
||||
$data['id'] = $result;
|
||||
}
|
||||
$inst = new $calling_class();
|
||||
$inst->hydrate($result);
|
||||
return $inst;
|
||||
}
|
||||
public static function find($key) {
|
||||
$calling_class = get_called_class();
|
||||
$inst = new $calling_class();
|
||||
$class = explode('\\',get_called_class());
|
||||
$tablename = strtolower($class[count($class)-1]);
|
||||
$query = new QueryBuilder();
|
||||
$query = $query->select('*')->from($tablename)->where($inst->primary_key,$key)->build();
|
||||
$result = ServiceContainer::Database()->select($query);
|
||||
if(count($result) == 0) return null;
|
||||
$inst->hydrate($result[0]);
|
||||
return $inst;
|
||||
}
|
||||
public function update($key) {
|
||||
$calling_class = get_called_class();
|
||||
$class = explode('\\',get_called_class());
|
||||
$tablename = strtolower($class[count($class)-1]);
|
||||
$query = new QueryBuilder();
|
||||
$query = $query->update($tablename)->set($key)->where($this->primary_key,$this->attributes[$this->primary_key])->build();
|
||||
$result = ServiceContainer::Database()->update($query);
|
||||
if(!$result) return null;
|
||||
else {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
public function delete() {
|
||||
$calling_class = get_called_class();
|
||||
$class = explode('\\',get_called_class());
|
||||
$tablename = strtolower($class[count($class)-1]);
|
||||
$query = new QueryBuilder();
|
||||
$query = $query->delete()->from($tablename)->where($this->primary_key,$this->attributes[$this->primary_key])->build();
|
||||
$result = ServiceContainer::Database()->update($query);
|
||||
if(!$result) return $this;
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function __get($prop) {
|
||||
return $this->attributes[$prop];
|
||||
}
|
||||
|
||||
function __set($prop, $val) {
|
||||
$this->attributes[$prop] = $val;
|
||||
}
|
||||
|
||||
}
|
82
Application/Foundations/QueryBuilder.php
Normal file
82
Application/Foundations/QueryBuilder.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
namespace Application\Foundations;
|
||||
|
||||
class QueryBuilder {
|
||||
private $query = "";
|
||||
private $where = "";
|
||||
public function select($fields) {
|
||||
if(!is_array($fields)) {
|
||||
$this->query .= "SELECT ".$fields;
|
||||
} else {
|
||||
$this->query .= "SELECT ".implode(",",SQLHelper::encode_list($fields));
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
public function delete() {
|
||||
$this->query .= "DELETE";
|
||||
return $this;
|
||||
}
|
||||
public function update($table) {
|
||||
// TODO: SQL injection
|
||||
$this->query .= "UPDATE ".$table;
|
||||
return $this;
|
||||
}
|
||||
public function set($data) {
|
||||
$this->query .= " SET";
|
||||
$final = [];
|
||||
foreach($data as $key => $value) {
|
||||
$final[] = $key." = ".SQLHelper::encode_literal($value);
|
||||
}
|
||||
$this->query .= " ".implode(",",$final);
|
||||
return $this;
|
||||
}
|
||||
public function from($table) {
|
||||
// TODO: SQL injection
|
||||
$this->query .= " FROM ".$table;
|
||||
return $this;
|
||||
}
|
||||
public function where($a, $b, $c = null) {
|
||||
$field = "";
|
||||
$value = "";
|
||||
$operator = "=";
|
||||
if($c == null) {
|
||||
// 2 param syntax
|
||||
$field = $a;
|
||||
$value = $b;
|
||||
} else {
|
||||
$field = $a;
|
||||
$value = $c;
|
||||
$operator = $b;
|
||||
}
|
||||
$value = SQLHelper::encode_literal($value);
|
||||
if($this->where == "") {
|
||||
$this->where .= " WHERE ".$field." ".$operator." ".$value;
|
||||
} else {
|
||||
$this->where .= " AND ".$field." ".$operator." ".$value;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
public function orWhere($a, $b, $c = null) {
|
||||
$field = "";
|
||||
$value = "";
|
||||
$operator = "=";
|
||||
if($c == null) {
|
||||
// 2 param syntax
|
||||
$field = $a;
|
||||
$value = $b;
|
||||
} else {
|
||||
$field = $a;
|
||||
$value = $c;
|
||||
$operator = $b;
|
||||
}
|
||||
if($this->where == "") {
|
||||
$this->where .= " WHERE ".$field." ".$operator." ".$value;
|
||||
} else {
|
||||
$this->where .= " OR ".$field." ".$operator." ".$value;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
public function build() {
|
||||
return $this->query.$this->where;
|
||||
}
|
||||
}
|
29
Application/Foundations/SQLHelper.php
Normal file
29
Application/Foundations/SQLHelper.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace Application\Foundations;
|
||||
|
||||
use Application\Services\ServiceContainer;
|
||||
|
||||
class SQLHelper {
|
||||
|
||||
public static function encode_list($data) {
|
||||
$insert_data = $data;
|
||||
foreach($insert_data as $index => $val) {
|
||||
$insert_data[$index] = SQLHelper::encode_literal($val);
|
||||
}
|
||||
return $insert_data;
|
||||
}
|
||||
public static function encode_literal($val) {
|
||||
$db = ServiceContainer::Database();
|
||||
if(is_numeric($val)) {
|
||||
return $val;
|
||||
} else if(is_null($val)) {
|
||||
return 'NULL';
|
||||
} else if(!is_numeric($val)) {
|
||||
return '"'.$db->escapeString($val).'"';
|
||||
} else if($val == "") {
|
||||
return '""';
|
||||
} else {
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
}
|
20
Application/HTTP/Request.php
Normal file
20
Application/HTTP/Request.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
namespace Application\HTTP;
|
||||
|
||||
class Request {
|
||||
private $data;
|
||||
private $query;
|
||||
public function __construct() {
|
||||
$this->data = $_REQUEST;
|
||||
$this->query = $_SERVER['QUERY_STRING'];
|
||||
}
|
||||
function queryString() {
|
||||
return $this->query;
|
||||
}
|
||||
function __get($prop) {
|
||||
return $this->data[$prop];
|
||||
}
|
||||
function __set($prop, $val) {
|
||||
$this->data[$prop] = $val;
|
||||
}
|
||||
}
|
48
Application/HTTP/Response.php
Normal file
48
Application/HTTP/Response.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
namespace Application\HTTP;
|
||||
|
||||
use Application\Services\ServiceContainer;
|
||||
|
||||
class Response {
|
||||
private $body = "";
|
||||
private $status = 200;
|
||||
private $headers = [];
|
||||
public function view($path, $args = []) {
|
||||
$this->body .= ServiceContainer::View()->render($path,$args);
|
||||
return $this;
|
||||
}
|
||||
public function data($data) {
|
||||
return $this->json()->body(json_encode($data));
|
||||
}
|
||||
public function body($body) {
|
||||
$this->body = $body;
|
||||
return $this;
|
||||
}
|
||||
public function statusCode($status) {
|
||||
$this->status = $status;
|
||||
return $this;
|
||||
}
|
||||
public function header($head) {
|
||||
$this->headers[] = $head;
|
||||
return $this;
|
||||
}
|
||||
public function json() {
|
||||
return $this->header('Content-Type: application/json');
|
||||
}
|
||||
public function render() {
|
||||
http_response_code($this->status);
|
||||
foreach($this->headers as $header) {
|
||||
header($header);
|
||||
}
|
||||
echo $this->body;
|
||||
}
|
||||
public function redirect($path) {
|
||||
return $this->header('Location: '.$path);
|
||||
}
|
||||
public function with($data) {
|
||||
foreach($data as $key => $val) {
|
||||
ServiceContainer::Session()->set($key,$val);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
}
|
8
Application/Models/User.php
Normal file
8
Application/Models/User.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace Application\Models;
|
||||
|
||||
use Application\Foundations\Model as DBModel;
|
||||
|
||||
class User extends DBModel {
|
||||
|
||||
}
|
8
Application/Models/UserConfirmation.php
Normal file
8
Application/Models/UserConfirmation.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace Application\Models;
|
||||
|
||||
use Application\Foundations\Model as DBModel;
|
||||
|
||||
class UserConfirmation extends DBModel {
|
||||
protected $primary_key = 'confirm_key';
|
||||
}
|
11
Application/Services/Authentication.php
Normal file
11
Application/Services/Authentication.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace Application\Services;
|
||||
|
||||
class Authentication {
|
||||
public function __construct() {
|
||||
ServiceContainer::Session();
|
||||
}
|
||||
public function isLoggedIn() {
|
||||
return ServiceContainer::Session()->has('user_id');
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
<?php
|
||||
namespace Mitsumine\Services;
|
||||
namespace Application\Services;
|
||||
|
||||
class Config {
|
||||
private $configs;
|
||||
public function __construct() {
|
||||
$this->configs = require 'backend/config.php';
|
||||
$this->configs = require 'config.php';
|
||||
}
|
||||
public function __call($name, $args) {
|
||||
return $this->configs[$name];
|
43
Application/Services/Database.php
Normal file
43
Application/Services/Database.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace Application\Services;
|
||||
|
||||
use Application\Services\ServiceContainer;
|
||||
use Application\Foundations\SQLHelper;
|
||||
|
||||
class Database {
|
||||
private $conn;
|
||||
private $config;
|
||||
public function __construct() {
|
||||
$this->config = ServiceContainer::Config();
|
||||
$this->conn = mysqli_connect($this->config->db_host(),$this->config->db_user(),$this->config->db_pass(),$this->config->db_name());
|
||||
}
|
||||
public function insert($table, $data) {
|
||||
$insert_data = SQLHelper::encode_list($data);
|
||||
$key_names = array_keys($insert_data);
|
||||
$query = "INSERT INTO ".$table." (".implode(",",$key_names).") VALUES (".implode(",",$insert_data).")";
|
||||
$result = mysqli_query($this->conn,$query);
|
||||
if($result) {
|
||||
return mysqli_insert_id($this->conn);
|
||||
} else {
|
||||
echo mysqli_error($this->conn);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public function update($query) {
|
||||
$result = mysqli_query($this->conn,$query);
|
||||
return $result;
|
||||
}
|
||||
public function select($query) {
|
||||
$result = mysqli_query($this->conn,$query);
|
||||
if($result) {
|
||||
return mysqli_fetch_all($result,MYSQLI_ASSOC);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Escaping strings requires DB connection, which is only handled by the Database service.
|
||||
public function escapeString($str) {
|
||||
return mysqli_real_escape_string($this->conn,$str);
|
||||
}
|
||||
}
|
10
Application/Services/Email.php
Normal file
10
Application/Services/Email.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace Application\Services;
|
||||
|
||||
use Application\Foundations\MailBuilder;
|
||||
|
||||
class Email {
|
||||
public static function send($email) {
|
||||
mail($email->to,$email->subject,$email->message,$email->headers);
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Mitsumine\Services;
|
||||
namespace Application\Services;
|
||||
|
||||
class ServiceContainer{
|
||||
private static $services = [];
|
||||
@ -10,7 +10,7 @@ class ServiceContainer{
|
||||
return self::$services[$service];
|
||||
}
|
||||
public static function load($service) {
|
||||
$class = 'Mitsumine\\Services\\'.$service;
|
||||
$class = 'Application\\Services\\'.$service;
|
||||
self::$services[$service] = new $class();
|
||||
}
|
||||
public static function __callStatic($name, $args) {
|
23
Application/Services/Session.php
Normal file
23
Application/Services/Session.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
namespace Application\Services;
|
||||
|
||||
class Session {
|
||||
public function __construct() {
|
||||
session_start();
|
||||
}
|
||||
public function get($path) {
|
||||
return $_SESSION[$path];
|
||||
}
|
||||
public function set($path, $val) {
|
||||
$_SESSION[$path] = $val;
|
||||
}
|
||||
public function unset($path) {
|
||||
$_SESSION[$path] = null;
|
||||
}
|
||||
public function has($path) {
|
||||
return isset($_SESSION[$path]);
|
||||
}
|
||||
public function destroy() {
|
||||
session_destroy();
|
||||
}
|
||||
}
|
14
Application/Services/View.php
Normal file
14
Application/Services/View.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace Application\Services;
|
||||
|
||||
class View {
|
||||
public function render($path, $args = []) {
|
||||
ob_start();
|
||||
extract($args);
|
||||
$auth = ServiceContainer::Authentication();
|
||||
include('Application/Views/'.$path.'.php');
|
||||
$rendered_string = ob_get_contents();
|
||||
ob_end_clean();
|
||||
return $rendered_string;
|
||||
}
|
||||
}
|
54642
Application/Static/css/metaforums.css
Normal file
54642
Application/Static/css/metaforums.css
Normal file
File diff suppressed because it is too large
Load Diff
9
Application/Views/index.php
Normal file
9
Application/Views/index.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
include 'layouts/head.php';
|
||||
?>
|
||||
|
||||
|
||||
|
||||
<?php
|
||||
include 'layouts/foot.php';
|
||||
?>
|
5
Application/Views/layouts/foot.php
Normal file
5
Application/Views/layouts/foot.php
Normal file
@ -0,0 +1,5 @@
|
||||
</main>
|
||||
<footer>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
23
Application/Views/layouts/head.php
Normal file
23
Application/Views/layouts/head.php
Normal file
@ -0,0 +1,23 @@
|
||||
<html>
|
||||
<head>
|
||||
<link href="/css/metaforums.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
|
||||
<title>Metaforums</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="header-left">
|
||||
<a href="/" class="header-link">Metaforums</a>
|
||||
</div>
|
||||
<div class="header-middle">
|
||||
</div>
|
||||
<div class="header-left">
|
||||
<?php if($auth->isLoggedIn()) { ?>
|
||||
<a href="/logout" class="header-link">Logout</a>
|
||||
<?php } else {?>
|
||||
<a href="/login" class="header-link">Login</a>
|
||||
<a href="/signup" class="header-link">Signup</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
52
Application/Views/login.php
Normal file
52
Application/Views/login.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
include 'layouts/head.php';
|
||||
?>
|
||||
<div id="login">
|
||||
<form action="/login" method="POST" id="login">
|
||||
<h1 class="form-title">Login</h1>
|
||||
<div class="input-group">
|
||||
<input type="text" name="username" id="username" placeholder="username" v-model="username"></input>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="password" name="password" id="password" placeholder="password" v-model="password"></input>
|
||||
</div>
|
||||
<div id="errors" v-if="errors != ''">
|
||||
<p>{{ errors }}</p>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<button type="submit" id="submit" @click="validate">Sign in</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
var app = new Vue({
|
||||
el: "#login",
|
||||
data: {
|
||||
username: "",
|
||||
password: "",
|
||||
errors: "<?php echo $_SESSION['errors'] ?? "" ?>",
|
||||
},
|
||||
methods: {
|
||||
validate: function(e) {
|
||||
// https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript
|
||||
var emailre = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
var emailvalid = emailre.test(String(this.email).toLowerCase()) ;
|
||||
|
||||
if(this.username == "") {
|
||||
this.errors = "Username must not be empty";
|
||||
e.preventDefault();
|
||||
return false;
|
||||
} else if(this.password == "") {
|
||||
this.errors = "Password must not be empty";
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
this.errors = "";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
include 'layouts/foot.php';
|
||||
?>
|
9
Application/Views/sign-up-success.php
Normal file
9
Application/Views/sign-up-success.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
include 'layouts/head.php';
|
||||
?>
|
||||
<h1>You are registered!</h1>
|
||||
<p>However, you will need to confirm your email address before you can start exploring the wonderful world of our forum</p>
|
||||
<p>Please check your inbox for further instructions</p>
|
||||
<?php
|
||||
include 'layouts/foot.php';
|
||||
?>
|
76
Application/Views/sign-up.php
Normal file
76
Application/Views/sign-up.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
include 'layouts/head.php';
|
||||
?>
|
||||
<div id="signup">
|
||||
<form action="/signup" method="POST" id="signup">
|
||||
<h1 class="form-title">Sign up</h1>
|
||||
<div class="input-group">
|
||||
<input type="text" name="email" id="email" placeholder="email" v-model="email"></input>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="text" name="username" id="username" placeholder="username" v-model="username"></input>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="password" name="password" id="password" placeholder="password" v-model="password"></input>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="password" name="confirmpassword" id="confirmpassword" placeholder="confirm password" v-model="confirmpassword"></input>
|
||||
</div>
|
||||
<div id="errors" v-if="errors != ''">
|
||||
<p>{{ errors }}</p>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<button type="submit" id="submit" @click="validate">Sign Up</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
var app = new Vue({
|
||||
el: "#signup",
|
||||
data: {
|
||||
email: "",
|
||||
username: "",
|
||||
password: "",
|
||||
confirmpassword: "",
|
||||
errors: "<?php echo $_SESSION['errors'] ?? "" ?>",
|
||||
},
|
||||
methods: {
|
||||
validate: function(e) {
|
||||
// https://stackoverflow.com/questions/46155/how-to-validate-an-email-address-in-javascript
|
||||
var emailre = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
var emailvalid = emailre.test(String(this.email).toLowerCase()) ;
|
||||
|
||||
if(this.email == "") {
|
||||
this.errors = "Email must not be empty";
|
||||
e.preventDefault();
|
||||
return false;
|
||||
} else if(!emailvalid) {
|
||||
this.errors = "Email is invalid";
|
||||
e.preventDefault();
|
||||
return false;
|
||||
} else if(this.username == "") {
|
||||
this.errors = "Username must not be empty";
|
||||
e.preventDefault();
|
||||
return false;
|
||||
} else if(this.username.length < 6 || this.username.length > 20) {
|
||||
this.errors = "Username must be between 6 and 20 characters";
|
||||
e.preventDefault();
|
||||
return false;
|
||||
} else if(this.password.length < 8) {
|
||||
this.errors = "Password must be at least 8 characters";
|
||||
e.preventDefault();
|
||||
return false;
|
||||
} else if(this.password !== this.confirmpassword) {
|
||||
this.errors = "Password and confirm password must be the same";
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
this.errors = "";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
include 'layouts/foot.php';
|
||||
?>
|
76
README.md
76
README.md
@ -1,27 +1,57 @@
|
||||
# Metaforums
|
||||
An online web-based discussion forum application
|
||||
|
||||
|
||||
## How to set up
|
||||
|
||||
1. Make sure mod_rewrite is enabled on Apache.
|
||||
2. Create a MySQL / MariaDB database, and import Metaforums' schema (`schema.sql`) into the database.
|
||||
3. Modify backend/config.php and adjust the configuration as needed.
|
||||
4. Point the browser to `localhost`
|
||||
|
||||
## Project Structure
|
||||
This project is separated between the frontend and the backend application.
|
||||
- backend/
|
||||
The backend of this application is written in PHP, and is API focused.
|
||||
- index.php
|
||||
The main handler of backend functions. Handles routing, loading of Mitsumine services, and conversion of array responses to JSON.
|
||||
- Mitsumine/
|
||||
Mitsumine is a set of custom-written helper classes to consolidate frequently used code.
|
||||
- Mitsumine/HTTP
|
||||
Mitsumine HTTP contains a number of abstractions for HTTP, such as Request class
|
||||
- Mitsumine/Services
|
||||
Mitsumine Services contains a number of service classes for common functionality such as Database and Session.
|
||||
- Application/
|
||||
Application contains classes that are the core of the application itself
|
||||
- Controllers/
|
||||
Controllers contain controllers that return HTTP responses
|
||||
- frontend/
|
||||
The frontend of this application, written in HTML and utilizes T
|
||||
|
||||
- index.php
|
||||
This index file allows serving both frontend and backend from one endpoint.
|
||||
|
||||
The main handler of backend functions. Handles routing, loading of Application services, and response handling.
|
||||
- Application/
|
||||
Application contains classes that are the core of the application itself
|
||||
- Assets/
|
||||
Assets contains buildable assets, for example source CSS.
|
||||
- Controllers/
|
||||
Controllers contain controllers that return HTTP responses
|
||||
- Foundations/
|
||||
Foundations are helper classes for various functions, such as an SQL query builder, and base model implementation;
|
||||
- SQLHelper
|
||||
Contains SQL escaping facilities.
|
||||
- QueryBuilder
|
||||
The SQL query builder.
|
||||
- Model
|
||||
The base model implementation. Contains common code for all models.
|
||||
- HTTP
|
||||
HTTP contains a number of abstractions for HTTP, such as Request class.
|
||||
- Models/
|
||||
Models contain database models.
|
||||
- Services/
|
||||
Services contains a number of service classes for common functionality such as Database and Session.
|
||||
- Authentication
|
||||
Provide auth related services.
|
||||
- Config
|
||||
Loads configuration and provides a facility to access the contents.
|
||||
- Database
|
||||
A service that centralizes database access.
|
||||
- Email
|
||||
A service for sending email.
|
||||
- ServiceContainer
|
||||
A service container that ensures every service is loaded once.
|
||||
- Session
|
||||
A service that contains centralized session management.
|
||||
- View
|
||||
Provides view rendering facilities
|
||||
- Static/
|
||||
Static contains static files served directly by the application. For example, this contains built CSS.
|
||||
- Views/
|
||||
Views contains all views used by the application
|
||||
|
||||
## Software Stack
|
||||
|
||||
The software is tested on the Apache server and PHP 7.3 on Arch Linux.
|
||||
@ -32,14 +62,6 @@ The database used is MariaDB 10.4.8
|
||||
|
||||
## External frontend libraries used
|
||||
|
||||
### Vue.js
|
||||
|
||||
Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web
|
||||
|
||||
Vue.js allows for interactivity while being less cumbersome than manipulating the DOM manually e.g. with jQuery.
|
||||
|
||||
[Project Website](https://vuejs.org)
|
||||
|
||||
### jQuery
|
||||
|
||||
jQuery is a feature-rich JavaScript library.
|
||||
|
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
namespace Application\Controllers;
|
||||
|
||||
use Mitsumine\HTTP\Request;
|
||||
|
||||
class IndexController {
|
||||
public function index(Request $request) {
|
||||
return [
|
||||
'mitsumine' => 'yuika'
|
||||
];
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
<?php
|
||||
namespace Mitsumine\HTTP;
|
||||
|
||||
class Request {
|
||||
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
namespace Mitsumine\Services;
|
||||
|
||||
use Mitsumine\Services\ServiceContainer;
|
||||
|
||||
class Database {
|
||||
private $conn;
|
||||
public function __construct() {
|
||||
$config = ServiceContainer::Config();
|
||||
$this->conn = mysqli_connect($config->db_host(),$config->db_user(),$config->db_pass(),$config->db_name());
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
require 'autoload.php';
|
||||
|
||||
// Use helper classes from Mitsumine
|
||||
use Mitsumine\HTTP\Request;
|
||||
use Mitsumine\Services\ServiceContainer;
|
||||
|
||||
ServiceContainer::Database();
|
||||
|
||||
// Get all routes
|
||||
$routes = require 'routes.php';
|
||||
|
||||
// Get request URI
|
||||
$uri = $_SERVER['PHP_SELF'];
|
||||
// Cut off index.php
|
||||
$uri = substr($uri,strlen('/index.php'),strlen($uri)-strlen('/index.php'));
|
||||
|
||||
// Build request object to pass to controller
|
||||
$request = new Request();
|
||||
|
||||
$request_method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// Get current route from uri
|
||||
$route = $routes[$request_method.':'.$uri];
|
||||
|
||||
|
||||
// Duar (actually, split the method string to class name and method name)
|
||||
$method_part = explode("@",$route['controller']);
|
||||
|
||||
// Get class name and method name
|
||||
$class = $method_part[0];
|
||||
$method = $method_part[1];
|
||||
|
||||
// Get fully qualified class name of route
|
||||
$fqcn = 'Application\\Controllers\\'.$class;
|
||||
$controller = new $fqcn();
|
||||
|
||||
// Execute method specified in route
|
||||
$result = $controller->$method($request);
|
||||
|
||||
// Convert array to JSON
|
||||
if(is_array($result)) {
|
||||
header('Content-Type: application/json');
|
||||
$result = json_encode($result);
|
||||
}
|
||||
echo $result;
|
||||
|
@ -1,6 +0,0 @@
|
||||
<?php
|
||||
return [
|
||||
'GET:/api' => [
|
||||
'controller' => 'IndexController@index',
|
||||
],
|
||||
];
|
@ -1,18 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
{{ message }}
|
||||
</div>
|
||||
<script>
|
||||
var app = new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
message: 'Yuika!'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
71
index.php
71
index.php
@ -1,18 +1,65 @@
|
||||
<?php
|
||||
|
||||
require 'autoload.php';
|
||||
|
||||
// Use helper classes from Application
|
||||
use Application\HTTP\Request;
|
||||
use Application\HTTP\Response;
|
||||
use Application\Services\ServiceContainer;
|
||||
|
||||
ServiceContainer::Database();
|
||||
ServiceContainer::Session();
|
||||
|
||||
// Get all routes
|
||||
$routes = require 'routes.php';
|
||||
|
||||
// Get request URI
|
||||
$uri = $_SERVER['PHP_SELF'];
|
||||
|
||||
// Cut off index.php
|
||||
$uri = substr($uri,strlen('/index.php'),strlen($uri)-strlen('/index.php'));
|
||||
if(strpos($uri,'/api') !== false && strpos($uri,'/api') == 0) {
|
||||
include 'backend/index.php';
|
||||
} else {
|
||||
// Remove trailing slashes
|
||||
if(substr($uri,strlen($uri)-1,1) == '/') {
|
||||
$uri = substr($uri,0,strlen($uri)-1);
|
||||
}
|
||||
$file = 'frontend'.$uri.'.html';
|
||||
if(!file_exists($file)) {
|
||||
$file = 'frontend'.$uri.'/index.html';
|
||||
}
|
||||
readfile($file);
|
||||
|
||||
// Serve static files first
|
||||
if(file_exists('Application/Static'.$uri) && $uri != '') {
|
||||
readfile('Application/Static'.$uri);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Remove trailing slash
|
||||
if(substr($uri,strlen($uri)-1,1) == '/') {
|
||||
$uri = substr($uri,0,strlen($uri)-1);
|
||||
}
|
||||
|
||||
// Build request object to pass to controller
|
||||
$request = new Request();
|
||||
|
||||
$response = new Response();
|
||||
|
||||
$request_method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// Get current route from uri
|
||||
if(!array_key_exists($request_method.':'.$uri,$routes)) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: application/json');
|
||||
die(json_encode([ 'success' => false, 'error' => 'Not found' ]));
|
||||
}
|
||||
|
||||
$route = $routes[$request_method.':'.$uri];
|
||||
|
||||
// Duar (actually, split the method string to class name and method name)
|
||||
$method_part = explode("@",$route['controller']);
|
||||
|
||||
// Get class name and method name
|
||||
$class = $method_part[0];
|
||||
$method = $method_part[1];
|
||||
|
||||
// Get fully qualified class name of route
|
||||
$fqcn = 'Application\\Controllers\\'.$class;
|
||||
$controller = new $fqcn();
|
||||
|
||||
$response = $controller->$method($request,$response);
|
||||
|
||||
$response->render();
|
||||
|
||||
// Convert array to JSON
|
||||
|
||||
|
27
routes.php
Normal file
27
routes.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
return [
|
||||
'GET:' => [
|
||||
'controller' => 'IndexController@index',
|
||||
],
|
||||
'GET:/signup' => [
|
||||
'controller' => 'AuthController@sign_up',
|
||||
],
|
||||
'POST:/signup' => [
|
||||
'controller' => 'AuthController@create_user',
|
||||
],
|
||||
'GET:/signup/success' => [
|
||||
'controller' => 'AuthController@sign_up_success',
|
||||
],
|
||||
'GET:/signup/confirm' => [
|
||||
'controller' => 'AuthController@sign_up_confirm',
|
||||
],
|
||||
'GET:/login' => [
|
||||
'controller' => 'AuthController@login',
|
||||
],
|
||||
'POST:/login' => [
|
||||
'controller' => 'AuthController@login_check',
|
||||
],
|
||||
'GET:/logout' => [
|
||||
'controller' => 'AuthController@logout',
|
||||
],
|
||||
];
|
Loading…
Reference in New Issue
Block a user