Skip to content

Commit

Permalink
Completed the database videos.
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkCLewis committed Apr 12, 2020
1 parent 60239e6 commit 8eb0422
Show file tree
Hide file tree
Showing 8 changed files with 336 additions and 16 deletions.
79 changes: 77 additions & 2 deletions server/app/controllers/TaskList5.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,87 @@ import scala.concurrent.ExecutionContext
import play.api.db.slick.HasDatabaseConfigProvider
import slick.jdbc.JdbcProfile
import slick.jdbc.PostgresProfile.api._
import scala.concurrent.Future

@Singleton
class TaskList5 @Inject() (protected val dbConfigProvider: DatabaseConfigProvider, cc: ControllerComponents)(implicit ec: ExecutionContext)
extends AbstractController(cc) with HasDatabaseConfigProvider[JdbcProfile] {


private val model = new TaskListDatabaseModel(db)

def load = Action { implicit request =>
Ok(views.html.version4Main())
Ok(views.html.version5Main())
}

implicit val userDataReads = Json.reads[UserData]
implicit val taskItemWrites = Json.writes[TaskItem]

def withJsonBody[A](f: A => Future[Result])(implicit request: Request[AnyContent], reads: Reads[A]): Future[Result] = {
request.body.asJson.map { body =>
Json.fromJson[A](body) match {
case JsSuccess(a, path) => f(a)
case e @ JsError(_) => Future.successful(Redirect(routes.TaskList3.load()))
}
}.getOrElse(Future.successful(Redirect(routes.TaskList3.load())))
}

def withSessionUsername(f: String => Future[Result])(implicit request: Request[AnyContent]): Future[Result] = {
request.session.get("username").map(f).getOrElse(Future.successful(Ok(Json.toJson(Seq.empty[String]))))
}

def withSessionUserid(f: Int => Future[Result])(implicit request: Request[AnyContent]): Future[Result] = {
request.session.get("userid").map(userid => f(userid.toInt)).getOrElse(Future.successful(Ok(Json.toJson(Seq.empty[String]))))
}

def validate = Action.async { implicit request =>
withJsonBody[UserData] { ud =>
model.validateUser(ud.username, ud.password).map { ouserId =>
ouserId match {
case Some(userid) =>
Ok(Json.toJson(true))
.withSession("username" -> ud.username, "userid" -> userid.toString, "csrfToken" -> play.filters.csrf.CSRF.getToken.map(_.value).getOrElse(""))
case None =>
Ok(Json.toJson(false))
}
}
}
}

def createUser = Action.async { implicit request =>
withJsonBody[UserData] { ud => model.createUser(ud.username, ud.password).map { ouserId =>
ouserId match {
case Some(userid) =>
Ok(Json.toJson(true))
.withSession("username" -> ud.username, "userid" -> userid.toString, "csrfToken" -> play.filters.csrf.CSRF.getToken.map(_.value).getOrElse(""))
case None =>
Ok(Json.toJson(false))
}
} }
}

def taskList = Action.async { implicit request =>
withSessionUsername { username =>
model.getTasks(username).map(tasks => Ok(Json.toJson(tasks)))
}
}

def addTask = Action.async { implicit request =>
withSessionUserid { userid =>
withJsonBody[String] { task =>
model.addTask(userid, task).map(count => Ok(Json.toJson(count > 0)))
}
}
}

def delete = Action.async { implicit request =>
withSessionUsername { username =>
withJsonBody[Int] { itemId =>
model.removeTask(itemId).map(removed => Ok(Json.toJson(removed)))
}
}
}

def logout = Action { implicit request =>
Ok(Json.toJson(true)).withSession(request.session - "username")
}
}
22 changes: 11 additions & 11 deletions server/app/models/Tables.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,29 @@ trait Tables {

/** Entity class storing rows of table Items
* @param itemId Database column item_id SqlType(serial), AutoInc, PrimaryKey
* @param userId Database column user_id SqlType(int4), Default(None)
* @param text Database column text SqlType(varchar), Length(2000,true), Default(None) */
case class ItemsRow(itemId: Int, userId: Option[Int] = None, text: Option[String] = None)
* @param userId Database column user_id SqlType(int4)
* @param text Database column text SqlType(varchar), Length(2000,true) */
case class ItemsRow(itemId: Int, userId: Int, text: String)
/** GetResult implicit for fetching ItemsRow objects using plain SQL queries */
implicit def GetResultItemsRow(implicit e0: GR[Int], e1: GR[Option[Int]], e2: GR[Option[String]]): GR[ItemsRow] = GR{
implicit def GetResultItemsRow(implicit e0: GR[Int], e1: GR[String]): GR[ItemsRow] = GR{
prs => import prs._
ItemsRow.tupled((<<[Int], <<?[Int], <<?[String]))
ItemsRow.tupled((<<[Int], <<[Int], <<[String]))
}
/** Table description of table items. Objects of this class serve as prototypes for rows in queries. */
class Items(_tableTag: Tag) extends profile.api.Table[ItemsRow](_tableTag, "items") {
def * = (itemId, userId, text) <> (ItemsRow.tupled, ItemsRow.unapply)
/** Maps whole row to an option. Useful for outer joins. */
def ? = ((Rep.Some(itemId), userId, text)).shaped.<>({r=>import r._; _1.map(_=> ItemsRow.tupled((_1.get, _2, _3)))}, (_:Any) => throw new Exception("Inserting into ? projection not supported."))
def ? = ((Rep.Some(itemId), Rep.Some(userId), Rep.Some(text))).shaped.<>({r=>import r._; _1.map(_=> ItemsRow.tupled((_1.get, _2.get, _3.get)))}, (_:Any) => throw new Exception("Inserting into ? projection not supported."))

/** Database column item_id SqlType(serial), AutoInc, PrimaryKey */
val itemId: Rep[Int] = column[Int]("item_id", O.AutoInc, O.PrimaryKey)
/** Database column user_id SqlType(int4), Default(None) */
val userId: Rep[Option[Int]] = column[Option[Int]]("user_id", O.Default(None))
/** Database column text SqlType(varchar), Length(2000,true), Default(None) */
val text: Rep[Option[String]] = column[Option[String]]("text", O.Length(2000,varying=true), O.Default(None))
/** Database column user_id SqlType(int4) */
val userId: Rep[Int] = column[Int]("user_id")
/** Database column text SqlType(varchar), Length(2000,true) */
val text: Rep[String] = column[String]("text", O.Length(2000,varying=true))

/** Foreign key referencing Users (database name items_user_id_fkey) */
lazy val usersFk = foreignKey("items_user_id_fkey", userId, Users)(r => Rep.Some(r.id), onUpdate=ForeignKeyAction.NoAction, onDelete=ForeignKeyAction.Cascade)
lazy val usersFk = foreignKey("items_user_id_fkey", userId, Users)(r => r.id, onUpdate=ForeignKeyAction.NoAction, onDelete=ForeignKeyAction.Cascade)
}
/** Collection-like TableQuery object for table Items */
lazy val Items = new TableQuery(tag => new Items(tag))
Expand Down
49 changes: 49 additions & 0 deletions server/app/models/TaskListDatabaseModel.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package models

import slick.jdbc.PostgresProfile.api._
import scala.concurrent.ExecutionContext
import models.Tables._
import scala.concurrent.Future
import org.mindrot.jbcrypt.BCrypt

class TaskListDatabaseModel(db: Database)(implicit ec: ExecutionContext) {
def validateUser(username: String, password: String): Future[Option[Int]] = {
val matches = db.run(Users.filter(userRow => userRow.username === username).result)
matches.map(userRows => userRows.headOption.flatMap {
userRow => if (BCrypt.checkpw(password, userRow.password)) Some(userRow.id) else None
})
}

def createUser(username: String, password: String): Future[Option[Int]] = {
val matches = db.run(Users.filter(userRow => userRow.username === username).result)
matches.flatMap { userRows =>
if (userRows.isEmpty) {
db.run(Users += UsersRow(-1, username, BCrypt.hashpw(password, BCrypt.gensalt())))
.flatMap { addCount =>
if (addCount > 0) db.run(Users.filter(userRow => userRow.username === username).result)
.map(_.headOption.map(_.id))
else Future.successful(None)
}
} else Future.successful(None)
}
}

def getTasks(username: String): Future[Seq[TaskItem]] = {
db.run(
(for {
user <- Users if user.username === username
item <- Items if item.userId === user.id
} yield {
item
}).result
).map(items => items.map(item => TaskItem(item.itemId, item.text)))
}

def addTask(userid: Int, task: String): Future[Int] = {
db.run(Items += ItemsRow(-1, userid, task))
}

def removeTask(itemId: Int): Future[Boolean] = {
db.run( Items.filter(_.itemId === itemId).delete ).map(count => count > 0)
}
}
3 changes: 2 additions & 1 deletion server/app/models/UserData.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
package models

case class UserData(username: String, password: String)
case class UserData(username: String, password: String)
case class TaskItem(id: Int, text: String)
14 changes: 14 additions & 0 deletions server/app/views/version5Main.scala.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@()(implicit request: RequestHeader, flash: Flash)

@main("Task List 5"){
<input type="hidden" id="csrfToken" value="@helper.CSRF.getToken.value">
<input type="hidden" id="validateRoute" value="@routes.TaskList5.validate()">
<input type="hidden" id="tasksRoute" value="@routes.TaskList5.taskList()">
<input type="hidden" id="createRoute" value="@routes.TaskList5.createUser()">
<input type="hidden" id="deleteRoute" value="@routes.TaskList5.delete()">
<input type="hidden" id="addRoute" value="@routes.TaskList5.addTask()">
<input type="hidden" id="logoutRoute" value="@routes.TaskList5.logout()">

<div id="react-root"></div>
<script src="@routes.Assets.versioned("javascript/version5.js")"></script>
}
9 changes: 9 additions & 0 deletions server/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ GET /chatSocket controllers.WebSocketChat.socket
#Routes for version 4
GET /load4 controllers.TaskList4.load

#Routes for version 5
GET /load5 controllers.TaskList5.load
POST /validate5 controllers.TaskList5.validate
POST /create5 controllers.TaskList5.createUser
GET /taskList5 controllers.TaskList5.taskList
POST /addTask5 controllers.TaskList5.addTask
POST /deleteTask5 controllers.TaskList5.delete
GET /logout5 controllers.TaskList5.logout


# Prefix must match `play.assets.urlPrefix`
GET /assets/*file controllers.Assets.at(file)
Expand Down
172 changes: 172 additions & 0 deletions server/public/javascript/version5.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
console.log("Running version 5.");

const ce = React.createElement
const csrfToken = document.getElementById("csrfToken").value;
const validateRoute = document.getElementById("validateRoute").value;
const tasksRoute = document.getElementById("tasksRoute").value;
const createRoute = document.getElementById("createRoute").value;
const deleteRoute = document.getElementById("deleteRoute").value;
const addRoute = document.getElementById("addRoute").value;
const logoutRoute = document.getElementById("logoutRoute").value;

class Version5MainComponent extends React.Component {
constructor(props) {
super(props);
this.state = { loggedIn: false };
}

render() {
if (this.state.loggedIn) {
return ce(TaskListComponent, { doLogout: () => this.setState( { loggedIn: false})});
} else {
return ce(LoginComponent, { doLogin: () => this.setState( { loggedIn: true }) });
}
}
}

class LoginComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loginName: "",
loginPass: "",
createName: "",
createPass: "",
loginMessage: "",
createMessage: ""
};
}

render() {
return ce('div', null,
ce('h2', null, 'Login:'),
ce('br'),
'Username: ',
ce('input', {type: "text", id: "loginName", value: this.state.loginName, onChange: e => this.changerHandler(e)}),
ce('br'),
'Password: ',
ce('input', {type: "password", id: "loginPass", value: this.state.loginPass, onChange: e => this.changerHandler(e)}),
ce('br'),
ce('button', {onClick: e => this.login(e)}, 'Login'),
ce('span', {id: "login-message"}, this.state.loginMessage),
ce('h2', null, 'Create User:'),
ce('br'),
'Username: ',
ce('input', {type: "text", id: "createName", value: this.state.createName, onChange: e => this.changerHandler(e)}),
ce('br'),
'Password: ',
ce('input', {type: "password", id: "createPass", value: this.state.createPass, onChange: e => this.changerHandler(e)}),
ce('br'),
ce('button', {onClick: e => this.createUser(e)}, 'Create User'),
ce('span', {id: "create-message"}, this.state.createMessage)
);
}

changerHandler(e) {
this.setState({ [e.target['id']]: e.target.value });
}

login(e) {
const username = this.state.loginName;
const password = this.state.loginPass;
fetch(validateRoute, {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Csrf-Token': csrfToken },
body: JSON.stringify({ username, password })
}).then(res => res.json()).then(data => {
if(data) {
this.props.doLogin();
} else {
this.setState({ loginMessage: "Login Failed" });
}
});
}

createUser() {
const username = this.state.createName;
const password = this.state.createPass;
fetch(createRoute, {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Csrf-Token': csrfToken },
body: JSON.stringify({ username, password })
}).then(res => res.json()).then(data => {
if(data) {
this.props.doLogin();
} else {
this.setState({ createMessage: "User Creation Failed"});
}
});
}
}

class TaskListComponent extends React.Component {
constructor(props) {
super(props);
this.state = { tasks: [], newTask: "", taskMessage: "" };
}

componentDidMount() {
this.loadTasks();
}

render() {
return ce('div', null,
'Task List',
ce('br'),
ce('ul', null,
this.state.tasks.map(task => ce('li', { key: task.id, onClick: e => this.handleDeleteClick(task.id) }, task.text))
),
ce('br'),
ce('div', null,
ce('input', {type: 'text', value: this.state.newTask, onChange: e => this.handleChange(e) }),
ce('button', {onClick: e => this.handleAddClick(e)}, 'Add Task'),
this.state.taskMessage
),
ce('br'),
ce('button', { onClick: e => this.props.doLogout() }, 'Log out')
);
}

loadTasks() {
fetch(tasksRoute).then(res => res.json()).then(tasks => this.setState({ tasks }));
}

handleChange(e) {
this.setState({newTask: e.target.value})
}

handleAddClick(e) {
fetch(addRoute, {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Csrf-Token': csrfToken },
body: JSON.stringify(this.state.newTask)
}).then(res => res.json()).then(data => {
if(data) {
this.loadTasks();
this.setState({ taskMessage: "", newTask: "" });
} else {
this.setState({ taskMessage: "Failed to add." });
}
});
}

handleDeleteClick(i) {
fetch(deleteRoute, {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Csrf-Token': csrfToken },
body: JSON.stringify(i)
}).then(res => res.json()).then(data => {
if(data) {
this.loadTasks();
this.setState({ taskMessage: "" });
} else {
this.setState({ taskMessage: "Failed to delete."});
}
});
}
}

ReactDOM.render(
ce(Version5MainComponent, null, null),
document.getElementById('react-root')
);
Loading

0 comments on commit 8eb0422

Please sign in to comment.