--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# View and controller of Tnitter
+module action
+
+import nitcorn
+
+import model
+
+redef class Session
+ # User logged in
+ var user: nullable String = null
+end
+
+# Main Tnitter Action
+class Tnitter
+ super Action
+
+ var db_path = "tnitter.db"
+ var db = new DB.open(db_path)
+
+ # Header on pages served by this `Action`
+ #
+ # Keywords to `Text::replace`:
+ # * `%app_path%` is the main URL to reach this `Action`
+ # * `%nav_right%` is the pulled right part of the header, used for login form
+ var header = """
+<nav class="navbar navbar-default" role="navigation">
+ <div class="container-fluid">
+ <div class="navbar-header">
+ <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
+ <span class="sr-only">Toggle navigation</span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </button>
+ <a class="navbar-brand" href="%app_path%">Tnitter</a>
+ </div>
+
+ <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
+ <ul class="nav navbar-nav">
+ <li><a href="https://github.com/privat/nit/">Nit repository</a></li>
+ </ul>
+
+ <ul class="nav navbar-nav pull-right">
+%header_right%
+ </ul>
+ </div>
+ </div>
+</nav>"""
+
+ # Template of the pages served by this `Action`
+ #
+ # Keywords to `Text::replace`:
+ # * The `%header%`, first thing in the `<body>`
+ # * The main page `%content%` within a `<div class="container">`
+ var template = """
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Tnitter</title>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+
+ <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
+ <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
+ <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
+</head>
+<body>
+
+%header%
+
+<div class="container">
+ %content%
+</div>
+</body>
+</html>"""
+
+ redef fun answer(request, turi)
+ do
+ # Get existing session
+ var session = request.session
+
+ # Error to display on page as a dismissable panel
+ var error = null
+
+ # Login/logout
+ if turi == "/login" and request.post_args.keys.has("user") and
+ request.post_args.keys.has("pass") then
+
+ var user = request.post_args["user"].trim
+ var pass = request.post_args["pass"]
+
+ var original_user = db.check_login(user, pass)
+ if original_user != null then
+ # Log in successful
+ if session == null then session = new Session
+ session.user = original_user
+ else
+ # Check for basic requirements
+ if user.is_empty then
+ error = "Username must have at least 1 character"
+ else if user.chars.has(' ') or user.chars.has('\n') then
+ error = "Username cannot contain white spaces"
+ else if db.sign_up(user, pass) then
+ # Sign up successful
+ if session == null then session = new Session
+ session.user = user
+ else
+ # Invalid user/pass
+ error = "Invalid combination of username and password"
+ session = null
+ end
+ end
+ else if turi == "/logout" then
+ # Logging out
+ session = null
+ else if turi == "/post" and request.post_args.keys.has("text") and session != null then
+ var user = session.user
+ if user != null then
+ # Post a Tnit!
+ var text = request.post_args["text"]
+ db.post(user, text)
+ end
+ end
+
+ var login_or_out
+ var content
+ if session == null or session.user == null then
+ # Log in form in the navbar
+ login_or_out = """
+<li>
+<form class="navbar-form" role="form" action="login" method="POST">
+ <div class="form-group">
+ <input type="text" placeholder="Username" class="form-control" name="user">
+ </div>
+ <div class="form-group">
+ <input type="password" placeholder="Password" class="form-control" name="pass">
+ </div>
+ <button type="submit" class="btn btn-default">Log in (or sign up)</button>
+</form>
+</li>
+ """
+
+ # Cannot post when not logged in
+ content = ""
+ else
+ # Log out form in the navbar
+ login_or_out = """
+<li><p class="navbar-text">Signed in as @{{{session.user.html_escape}}}</p></li>
+<li>
+<form class="navbar-form" role="form" action="logout" method="POST">
+ <button type="submit" class="btn btn-default">Log out</button>
+</form>
+</li>
+ """
+
+ # Post form
+ content = """
+<form class="form" role="form" action="post" method="POST">
+ <div class="form-group">
+ <div class="input-group">
+ <div class="input-group-addon">Share your thoughts</div>
+ <input class="form-control" type="text" placeholder="..." name="text">
+ <span class="input-group-btn">
+ <button class="btn btn-default" type="submit">Tnit!</button>
+ </span>
+ </div><!-- /input-group -->
+ </div>
+</form>
+ """
+ end
+
+ # Show error if any
+ var error_html
+ if error != null then
+ error_html = """
+<div class="alert alert-danger alert-dismissible" role="alert">
+ <button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
+ {{{error}}}
+</div>
+ """
+ else error_html = ""
+
+ # Load the last 16 Tnits
+ var posts = db.latest_posts(16)
+
+ var html_posts = new Array[String]
+ for post in posts do
+ html_posts.add "<tr><td>@{post.user.html_escape}</td><td>{post.text.html_escape}</td></tr>"
+ end
+
+ content += """
+<div class="panel panel-default">
+ <div class="panel-heading">Latest Tnits</div>
+ <table class="table table-striped">
+ {{{html_posts.join("\n")}}}
+ </table>
+ </div>
+</div>
+ """
+
+ # Get page from template, we replace the header first so we can replace
+ # everything on the same body afterwards
+ var body = template.
+ replace("%header%", header).
+ replace("%app_path%", request.uri.strip_extension(turi) + "/").
+ replace("%header_right%", login_or_out).
+ replace("%content%", error_html + content)
+
+ # Build response
+ var response = new HttpResponse(200)
+ response.body = body
+ response.session = session
+ return response
+ end
+end
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Data and DB model of Tnitter
+module model
+
+import sqlite3
+import md5
+
+# The Tnitter database
+class DB
+ super Sqlite3DB
+
+ redef init open(path)
+ do
+ super
+ create_tables
+ end
+
+ # Create the needed tables
+ fun create_tables
+ do
+ assert create_table("IF NOT EXISTS users (user TEXT PRIMARY KEY, pass TEXT)") else
+ print error or else "?"
+ end
+
+ assert create_table("IF NOT EXISTS posts (user TEXT, text TEXT, posted DATETIME DEFAULT CURRENT_TIMESTAMP)") else
+ print error or else "?"
+ end
+ end
+
+ # Check if the login credentials are valid
+ #
+ # If valid, returns the username at database creation time. Otherwise returns `null`.
+ fun check_login(user, pass: String): nullable String
+ do
+ var stmt = select("user FROM users WHERE lower({user.to_sql_string}) = lower(user) " +
+ "AND {pass.tnitter_hash.to_sql_string} = pass")
+ assert stmt != null else print error or else "?"
+
+ for row in stmt do return row[0].to_s
+ return null
+ end
+
+ # Try to sign up a new user, return `true` on success
+ fun sign_up(user, pass: String): Bool
+ do
+ # Check if already in user
+ var stmt = select("user FROM users WHERE lower({user.to_sql_string}) = lower(user)")
+ assert stmt != null else print error or else "?"
+
+ if not stmt.iterator.to_a.is_empty then return false
+
+ # Insert intro BD
+ assert insert("INTO users(user, pass) VALUES ({user.to_sql_string}, {pass.tnitter_hash.to_sql_string})") else
+ print error or else "?"
+ end
+
+ return true
+ end
+
+ # Tnit something
+ fun post(user, text: String)
+ do
+ assert insert("INTO posts(user, text) VALUES ({user.to_sql_string}, {text.to_sql_string})") else
+ print error or else "?"
+ end
+ end
+
+ # Get the latest tnits
+ fun latest_posts(count: Int): Array[Post]
+ do
+ var stmt = select("user, text FROM posts ORDER BY datetime(posted) DESC LIMIT {count}")
+ assert stmt != null else print error or else "?"
+
+ var posts = new Array[Post]
+ for row in stmt do posts.add new Post(row[0].to_s, row[1].to_s)
+
+ return posts
+ end
+end
+
+# A single post (or Tnit)
+class Post
+ # The author
+ var user: String
+
+ # Tnit content
+ var text: String
+end
+
+redef class String
+ # Hash passwords for Tnitter
+ fun tnitter_hash: String do return (self+salt).md5
+end
+
+# Salt used on passwords
+#
+# Can be redefed by user modules
+fun salt: String do return "tnitter is cool"
--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Web server part of the Tnitter project
+module tnitter
+
+import privileges
+
+import model
+import action
+
+redef class OptionContext
+ var drop = new OptionUserAndGroup.for_dropping_privileges
+ var help = new OptionBool("Print this message", "--help", "-h")
+
+ init do add_option(drop, help)
+end
+
+# Avoid executing when running tests
+if "NIT_TESTING".environ == "true" then exit 0
+
+# Prepare options
+var opts = new OptionContext
+opts.parse(args)
+if not opts.errors.is_empty or opts.help.value then
+ print opts.errors
+ print "Usage: tnitter [Options]"
+ opts.usage
+ exit 1
+end
+
+# If we can, we use port 80
+var interfac
+if sys.uid == 0 then # Are we root?
+ interfac = "localhost:80"
+else interfac = "localhost:8080"
+
+# Setup server
+var vh = new VirtualHost(interfac)
+var factory = new HttpFactory.and_libevent
+factory.config.virtual_hosts.add vh
+
+# Drop to a low-privileged user
+var user_group = opts.drop.value
+if user_group != null then user_group.drop_privileges
+
+# Complete server config
+vh.routes.add new Route(null, new Tnitter)
+
+# Run
+print "Launching server on http://{interfac} ..."
+factory.run