diff --git a/README.md b/README.md
index 4298e1e6..50d1eb5c 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,15 @@
# Coder
-## A simple way to make web stuff on Raspberry Pi
+## A simple way to make web stuff.
+### This is a fork of the original Raspberry Pi version that will allow our coder dojo to distribute and run Google Coder for people to use on Macs and PCs. This also includes starter projects used for classes in our dojo.
+This bundle helps to create a "portable distribution" for a targetted platform. You need to have NodeJS installed on the platform you want to build for. Go into coder-apps and run "bundle_win.cmd" (Windows), or "bundle_mac.sh" Mac/Linux. The resulting folder can be zipped up and distributed onto a machine with either a local copy of the NodeJS executable or will operate with a full NodeJS install.
+
+Note: There appear to be some issues with the bundle_win.cmd on Windows 7.
+---
+
Coder is a free piece of software that turns a Raspberry Pi into a super simple platform that educators and parents can use to teach the basics of building for the web. New coders can craft small projects in HTML, CSS, and Javascript, right from the web browser.
+For the Original Raspberry Pi version, click here:
http://goo.gl/coder
### What You'll Find Here
diff --git a/coder-apps/bundle_mac.sh b/coder-apps/bundle_mac.sh
new file mode 100644
index 00000000..bf31516d
--- /dev/null
+++ b/coder-apps/bundle_mac.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+##
+## Creates the bundle for the Mac
+##
+## sh bundle-mac.sh base_path
+##
+## Eg.
+## sh bundle-mac.sh ../coder-base/
+
+if [ $# != 1 ]
+ then
+ echo -e "\nUse:\nbundle-mac.sh coderbase\n"
+ exit
+fi
+
+base=$1
+
+mkdir -p $base
+
+## Copy the startup script to the base folder
+cp ../coderdojo/scripts/run-coder.sh $base
+
+## Expand NodeJS bundle and install it in the base folder.
+mkdir -p ./tmp
+cp ../coderdojo/nodejs/node-mac.tar.gz ./tmp
+cd ./tmp
+gunzip node-mac.tar.gz
+tar -xvf node-mac.tar
+cd ..
+mkdir -p $base/node
+cp -R ./tmp/nodejs/* $base/node
+rm -fr ./tmp
+
+## do the rest of the installation process
+./install_all.sh $base
+cd $base
+
+
diff --git a/coder-apps/bundle_win.cmd b/coder-apps/bundle_win.cmd
new file mode 100644
index 00000000..f0443dc6
--- /dev/null
+++ b/coder-apps/bundle_win.cmd
@@ -0,0 +1,19 @@
+@echo off
+if =%1-==-- echo "Usage: bundle_win.cmd [coderbase]" & exit /b
+
+set base=%1
+
+REM Create base folder
+IF NOT EXIST %base% (
+ mkdir %base%
+)
+
+mkdir %base%
+copy scripts\run-coder.cmd %base%
+
+mkdir %base%\node
+copy node\node.exe %base%\node
+
+call install_all.cmd %base%
+
+cd %base%
diff --git a/coder-apps/common/auth/app/app.js b/coder-apps/common/auth/app/app.js
index dacd9034..c4357c14 100644
--- a/coder-apps/common/auth/app/app.js
+++ b/coder-apps/common/auth/app/app.js
@@ -21,7 +21,7 @@
var mustache = require('mustache');
var util = require('util');
var fs = require('fs');
-var bcrypt = require('bcrypt');
+var bcrypt = require('bcrypt-nodejs');
//stores cache of password hash and device name
var device_settings = {
diff --git a/coder-apps/common/coder/views/index.html b/coder-apps/common/coder/views/index.html
index d762b841..09db8d56 100644
--- a/coder-apps/common/coder/views/index.html
+++ b/coder-apps/common/coder/views/index.html
@@ -75,7 +75,9 @@
Settings
+
Your Coder's Name
@@ -112,9 +114,11 @@
Settings
+
Save
Cancel
diff --git a/coder-apps/common/coderlib/app/app.js b/coder-apps/common/coderlib/app/app.js
index 6ec223e8..21b561e9 100644
--- a/coder-apps/common/coderlib/app/app.js
+++ b/coder-apps/common/coderlib/app/app.js
@@ -69,9 +69,11 @@ exports.listApps = function() {
var path = process.cwd(); //root application path. different from __dirname
var appdir = path + "/apps/";
var apps = {};
+ console.log ("reading apps from: " + appdir);
var files = fs.readdirSync(appdir);
for ( var x in files ) {
var filename = files[x];
+ console.log ('Processing: ' + filename);
var info = fs.statSync( appdir + filename );
if ( info.isDirectory() ) {
var appinfo = null;
diff --git a/coder-apps/common/comic_creator/app/app.js b/coder-apps/common/comic_creator/app/app.js
new file mode 100644
index 00000000..5e949e05
--- /dev/null
+++ b/coder-apps/common/comic_creator/app/app.js
@@ -0,0 +1,30 @@
+exports.settings={};
+//These are dynamically updated by the runtime
+//settings.appname - the app id (folder) where your app is installed
+//settings.viewpath - prefix to where your view html files are located
+//settings.staticurl - base url path to static assets /static/apps/appname
+//settings.appurl - base url path to this app /app/appname
+//settings.device_name - name given to this coder by the user, Ie."Billy's Coder"
+//settings.coder_owner - name of the user, Ie. "Suzie Q."
+//settings.coder_color - hex css color given to this coder.
+
+exports.get_routes = [
+ { path:'/', handler:'index_handler' },
+];
+
+exports.post_routes = [
+];
+
+
+exports.index_handler = function( req, res ) {
+ var tmplvars = {};
+ tmplvars['static_url'] = exports.settings.staticurl;
+ tmplvars['app_name'] = exports.settings.appname;
+ tmplvars['app_url'] = exports.settings.appurl;
+ tmplvars['device_name'] = exports.settings.device_name;
+
+ res.render( exports.settings.viewpath + '/index', tmplvars );
+};
+
+exports.on_destroy = function() {
+};
\ No newline at end of file
diff --git a/coder-apps/common/comic_creator/app/meta.json b/coder-apps/common/comic_creator/app/meta.json
new file mode 100644
index 00000000..d08712ce
--- /dev/null
+++ b/coder-apps/common/comic_creator/app/meta.json
@@ -0,0 +1,8 @@
+{
+ "created": "2013-12-06",
+ "modified": "2013-12-20",
+ "color": "#95a5a6",
+ "author": "Coder Team",
+ "name": "Comic Creator",
+ "hidden": false
+}
\ No newline at end of file
diff --git a/coder-apps/common/comic_creator/static/css/index.css b/coder-apps/common/comic_creator/static/css/index.css
new file mode 100644
index 00000000..dfc6986c
--- /dev/null
+++ b/coder-apps/common/comic_creator/static/css/index.css
@@ -0,0 +1,158 @@
+/*This sets the background color for our comic page*/
+
+html body {
+ background-color: #2b2b2b;
+}
+
+
+/*These IDs are for the invisible divs that will hold our comic together*/
+
+#page {
+ height: 100%;
+ width: 800px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+#titleblock {
+ height: 160px;
+ width: 800px;
+ padding-left: 10px;
+}
+
+/*These are the text styles for the headline and byline*/
+
+h1 {
+ font-family: "Comic Sans MS";
+ font-style: italic;
+ font-weight: 900;
+ font-size: 45px;
+ color: white;
+}
+
+h2 {
+ font-family: "Comic Sans MS";
+ font-style: italic;
+ font-weight: 900;
+ font-size: 16px;
+ color: white;
+}
+
+/*These IDs are for the panels that will make up our comic.*/
+
+#panel1 {
+ position:relative;
+ height: 280px;
+ width: 780px;
+ border:10px solid #2b2b2b;
+ float: left;
+ background-color: #28d8eb;
+ overflow: hidden;
+}
+
+#panel2 {
+ position:relative;
+ height: 280px;
+ width: 280px;
+ border:10px solid #2b2b2b;
+ float: left;
+ background-color: #77da62;
+ overflow: hidden;
+}
+
+#panel3 {
+ position:relative;
+ height: 280px;
+ width: 480px;
+ border:10px solid #2b2b2b;
+ float: left;
+ background-color: #00d26d;
+ overflow: hidden;
+}
+
+#panel4 {
+ position:relative;
+ height: 280px;
+ width: 480px;
+ border:10px solid #2b2b2b;
+ float: left;
+ background-color: #ffc100;
+ overflow: hidden;
+}
+
+#panel5 {
+ position:relative;
+ height: 280px;
+ width: 280px;
+ border:10px solid #2b2b2b;
+ float: left;
+ background-color: #ff9100;
+ overflow: hidden;
+}
+
+/*These classes are for the different text styles we will be using in the comic*/
+
+.narration {
+ font-family: "Comic Sans MS";
+ font-style: italic;
+ font-weight: 900;
+ font-size: 15px;
+ padding: 15px;
+ color: white;
+ margin: 10px;
+ background-color: #2b2b2b;
+}
+
+.bigtext {
+ font-family: "Comic Sans MS";
+ color: white;
+ font-style: italic;
+ font-weight: 900;
+ font-size: 30px;
+ line-height: 100%;
+ margin: 20px;
+ text-shadow:2px 2px #2b2b2b;
+}
+
+/* These classes can position text and images within the panel.*/
+
+.left {
+ position:absolute;
+ left: 0;
+}
+
+.right {
+ position:absolute;
+ right: 0;
+}
+
+.top {
+ position:absolute;
+ top: 0;
+}
+
+.bottom {
+ position:absolute;
+ bottom: 0;
+}
+
+/*These ids can be used fit specific images into our panels based on the dimensions of the image*/
+
+#image1 {
+ position: absolute;
+}
+
+/*These classes are for stacking or ordering elements on top of eachother.*/
+
+.inback {
+ z-index: 1;
+}
+
+.inmiddle {
+ z-index: 2;
+}
+
+.infront{
+ z-index: 3;
+}
+
diff --git a/coder-apps/common/comic_creator/static/js/index.js b/coder-apps/common/comic_creator/static/js/index.js
new file mode 100644
index 00000000..89508e79
--- /dev/null
+++ b/coder-apps/common/comic_creator/static/js/index.js
@@ -0,0 +1,6 @@
+
+$(document).ready( function() {
+
+ //This code will run after your page loads
+
+});
\ No newline at end of file
diff --git a/coder-apps/common/comic_creator/static/media/panel_1.jpg b/coder-apps/common/comic_creator/static/media/panel_1.jpg
new file mode 100644
index 00000000..36923439
Binary files /dev/null and b/coder-apps/common/comic_creator/static/media/panel_1.jpg differ
diff --git a/coder-apps/common/comic_creator/static/media/panel_2.jpg b/coder-apps/common/comic_creator/static/media/panel_2.jpg
new file mode 100644
index 00000000..0649c429
Binary files /dev/null and b/coder-apps/common/comic_creator/static/media/panel_2.jpg differ
diff --git a/coder-apps/common/comic_creator/static/media/panel_3.jpg b/coder-apps/common/comic_creator/static/media/panel_3.jpg
new file mode 100644
index 00000000..32886197
Binary files /dev/null and b/coder-apps/common/comic_creator/static/media/panel_3.jpg differ
diff --git a/coder-apps/common/comic_creator/static/media/panel_4.jpg b/coder-apps/common/comic_creator/static/media/panel_4.jpg
new file mode 100644
index 00000000..7a603491
Binary files /dev/null and b/coder-apps/common/comic_creator/static/media/panel_4.jpg differ
diff --git a/coder-apps/common/comic_creator/static/media/panel_5.jpg b/coder-apps/common/comic_creator/static/media/panel_5.jpg
new file mode 100644
index 00000000..4821f06c
Binary files /dev/null and b/coder-apps/common/comic_creator/static/media/panel_5.jpg differ
diff --git a/coder-apps/common/comic_creator/views/index.html b/coder-apps/common/comic_creator/views/index.html
new file mode 100644
index 00000000..eefd0e88
--- /dev/null
+++ b/coder-apps/common/comic_creator/views/index.html
@@ -0,0 +1,59 @@
+
+
+
+ Coder
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Your Title Here
+
Your Name And Credits Here
+
+
+
This is for narration and scene-setting.
+
This is talking or a sound effect.
+
+
+
+
More narration.
+
More talking.
+
+
+
+
More narration.
+
More talking.
+
+
+
+
More narration.
+
More talking.
+
+
+
+
More narration.
+
More talking.
+
+
+
+
+
\ No newline at end of file
diff --git a/coder-apps/common/localauth/app/app.js b/coder-apps/common/localauth/app/app.js
new file mode 100644
index 00000000..55e8ca6c
--- /dev/null
+++ b/coder-apps/common/localauth/app/app.js
@@ -0,0 +1,651 @@
+/**
+ * Coder for Raspberry Pi
+ * A simple platform for experimenting with web stuff.
+ * http://goo.gl/coder
+ *
+ * Copyright 2013 Google Inc. All Rights Reserved.
+ *
+ * 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.
+ */
+
+var mustache = require('mustache');
+var util = require('util');
+var fs = require('fs');
+// var bcrypt = require('bcrypt-nodejs');
+
+//stores cache of password hash and device name
+var device_settings = {
+ password_hash: '',
+ device_name: '',
+ hostname: '',
+ coder_owner: '',
+ coder_color: '#3e3e3e'
+};
+
+
+exports.settings={};
+//These are dynamically updated by the runtime
+//settings.appname - the app id (folder) where your app is installed
+//settings.viewpath - prefix to where your view html files are located
+//settings.staticurl - base url path to static assets /static/apps/appname
+//settings.appurl - base url path to this app /app/appname
+//settings.device_name - name the user gave to their coder "Susie's Coder"
+
+
+exports.get_routes = [
+ { path:'/', handler:'index_handler'},
+/*
+ { path:'/login', handler:'login_handler'},
+ { path:'/logout', handler:'logout_handler'},
+*/
+ { path:'/configure', handler:'configure_handler'},
+/*
+ { path:'/addpassword', handler:'addpassword_handler'},
+ { path:'/changepassword', handler:'changepassword_handler'},
+*/
+ { path: '/api/devicename/get', handler: 'api_devicename_get_handler' },
+ { path: '/api/codercolor/get', handler: 'api_codercolor_get_handler' },
+ { path: '/api/coderowner/get', handler: 'api_coderowner_get_handler' }
+];
+
+
+exports.post_routes = [
+/*
+ { path: '/api/login', handler: 'api_login_handler' },
+ { path: '/api/logout', handler: 'api_logout_handler' },
+*/
+ { path: '/api/devicename/set', handler: 'api_devicename_set_handler' },
+ { path: '/api/codercolor/set', handler: 'api_codercolor_set_handler' },
+ { path: '/api/coderowner/set', handler: 'api_coderowner_set_handler' } // ,
+/*
+ { path: '/api/addpassword', handler: 'api_addpassword_handler' },
+ { path: '/api/changepassword', handler: 'api_changepassword_handler' }
+*/
+];
+
+exports.on_destroy = function() {
+};
+
+/*
+exports.isAuthenticated = function( req ) {
+ if ( typeof req.session !== 'undefined' && typeof req.session.authenticated !== 'undefined' ) {
+ return req.session.authenticated === true;
+ }
+ return false;
+};
+*/
+
+exports.isConfigured = function() {
+ if ( typeof device_settings.device_name !== 'undefined' && device_settings.device_name !== '' &&
+ typeof device_settings.hostname !== 'undefined' && device_settings.hostname !== '' ) {
+ return true;
+ } else {
+ return false;
+ }
+};
+
+/*
+exports.hasPassword = function() {
+ if ( typeof device_settings.password_hash !== 'undefined' && device_settings.password_hash !== '' ) {
+ return true;
+ } else {
+ return false;
+ }
+};
+*/
+
+exports.getDeviceName = function() {
+ return device_settings.device_name;
+};
+exports.getCoderOwner = function() {
+ return device_settings.coder_owner;
+};
+exports.getCoderColor = function() {
+ return device_settings.coder_color;
+};
+
+/*
+exports.authenticate = function( req, password ) {
+
+ var authenticated = bcrypt.compareSync( password, device_settings.password_hash );
+ if ( authenticated ) {
+ req.session.authenticated = true;
+ }
+
+ return authenticated;
+};
+
+exports.logout = function( req ) {
+
+ req.session.authenticated = false;
+};
+*/
+
+exports.index_handler = function( req, res ) {
+
+ var firstuse = "?firstuse";
+ if ( typeof( req.param('firstuse') ) === 'undefined' ) {
+ firstuse = "";
+ }
+
+ if ( !exports.isConfigured() ) {
+ res.redirect('/app/auth/configure?firstuse');
+/*
+ } else if ( !exports.hasPassword() ) {
+ res.redirect('/app/auth/addpassword?firstuse');
+ } else if ( !exports.isAuthenticated(req) ) {
+ res.redirect('/app/auth/login' + firstuse);
+*/
+ } else {
+ res.redirect('/app/coder' + firstuse);
+ }
+};
+
+/*
+exports.addpassword_handler = function( req, res ) {
+ var tmplvars = {};
+ tmplvars['static_url'] = exports.settings.staticurl;
+ tmplvars['app_name'] = exports.settings.appname;
+ tmplvars['app_url'] = exports.settings.appurl;
+ tmplvars['device_name'] = exports.settings.device_name;
+ tmplvars['page_mode'] = "addpassword";
+
+ //only allow this step if they have not yet set a password
+ if ( !exports.hasPassword() ) {
+ res.render( exports.settings.viewpath + '/index', tmplvars );
+ } else {
+ res.redirect('/app/auth/login');
+ }
+};
+
+exports.changepassword_handler = function( req, res ) {
+ var tmplvars = {};
+ tmplvars['static_url'] = exports.settings.staticurl;
+ tmplvars['app_name'] = exports.settings.appname;
+ tmplvars['app_url'] = exports.settings.appurl;
+ tmplvars['device_name'] = exports.settings.device_name;
+ tmplvars['page_mode'] = "changepassword";
+
+ //only allow this step if they are authenticated
+ if ( exports.isAuthenticated(req) ) {
+ res.render( exports.settings.viewpath + '/index', tmplvars );
+ } else {
+ res.redirect('/app/auth/login');
+ }
+};
+*/
+
+exports.configure_handler = function( req, res ) {
+ var tmplvars = {};
+ tmplvars['static_url'] = exports.settings.staticurl;
+ tmplvars['app_name'] = exports.settings.appname;
+ tmplvars['app_url'] = exports.settings.appurl;
+ tmplvars['device_name'] = exports.settings.device_name;
+ tmplvars['page_mode'] = "configure";
+
+/*
+ //only allow this step if they are authenticated or have not yet set a password
+ if ( exports.isAuthenticated(req) || !exports.hasPassword() ) {
+ res.render( exports.settings.viewpath + '/index', tmplvars );
+ } else {
+ res.redirect('/app/auth/login');
+ }
+*/
+ res.render( exports.settings.viewpath + '/index', tmplvars );
+};
+
+exports.api_devicename_get_handler = function( req, res ) {
+ res.json({
+ device_name: exports.getDeviceName()
+ });
+};
+exports.api_codercolor_get_handler = function( req, res ) {
+ res.json({
+ coder_color: exports.getCoderColor()
+ });
+};
+exports.api_coderowner_get_handler = function( req, res ) {
+ //only allow this step if they are authenticated or have not yet set a password
+ if ( !exports.isAuthenticated(req) && exports.hasPassword() ) {
+ res.json({
+ status: "error",
+ error: "not authenticated"
+ });
+ return;
+ }
+ res.json({
+ coder_owner: exports.getCoderOwner()
+ });
+};
+
+exports.api_devicename_set_handler = function( req, res ) {
+
+ //only allow this step if they are authenticated or have not yet set a password
+ if ( !exports.isAuthenticated(req) && exports.hasPassword() ) {
+ res.json({
+ status: "error",
+ error: "not authenticated"
+ });
+ return;
+ }
+
+ var devicename = req.param('device_name');
+ if ( !devicename || devicename === "" || !isValidDeviceName( devicename ) ) {
+ res.json({
+ status: 'error',
+ error: "invalid device name"
+ });
+ return;
+ }
+
+ device_settings.device_name = devicename;
+ device_settings.hostname = hostnameFromDeviceName( devicename );
+
+ err = saveDeviceSettings();
+
+ if ( !err ) {
+ res.json({
+ status: "success",
+ device_name: device_settings.device_name,
+ hostname: device_settings.hostname
+ });
+ } else {
+ res.json({
+ status: "error",
+ error: "could not save device settings"
+ });
+ }
+
+};
+
+
+exports.api_coderowner_set_handler = function( req, res ) {
+
+ //only allow this step if they are authenticated or have not yet set a password
+ if ( !exports.isAuthenticated(req) && exports.hasPassword() ) {
+ res.json({
+ status: "error",
+ error: "not authenticated"
+ });
+ return;
+ }
+
+ var owner = req.param('coder_owner');
+ if ( typeof owner === 'undefined' ) {
+ res.json({
+ status: 'error',
+ error: "invalid owner name"
+ });
+ return;
+ }
+
+ device_settings.coder_owner = owner;
+
+ err = saveDeviceSettings();
+
+ if ( !err ) {
+ res.json({
+ status: "success",
+ coder_owner: device_settings.coder_owner
+ });
+ } else {
+ res.json({
+ status: "error",
+ error: "could not save device settings"
+ });
+ }
+
+};
+
+exports.api_codercolor_set_handler = function( req, res ) {
+
+ //only allow this step if they are authenticated or have not yet set a password
+ if ( !exports.isAuthenticated(req) && exports.hasPassword() ) {
+ res.json({
+ status: "error",
+ error: "not authenticated"
+ });
+ return;
+ }
+
+ var color = req.param('coder_color');
+ if ( typeof color === 'undefined' || !isValidColor( color ) ) {
+ res.json({
+ status: 'error',
+ error: "invalid color"
+ });
+ return;
+ }
+
+ device_settings.coder_color = color;
+
+ err = saveDeviceSettings();
+
+ if ( !err ) {
+ res.json({
+ status: "success",
+ coder_color: device_settings.coder_color
+ });
+ } else {
+ res.json({
+ status: "error",
+ error: "could not save device settings"
+ });
+ }
+
+};
+
+/*
+exports.api_addpassword_handler = function( req, res ) {
+
+ //only allow this step if they have not yet set a password
+ if ( exports.hasPassword() ) {
+ res.json({
+ status: "error",
+ error: "not authenticated"
+ });
+ return;
+ }
+
+ var pass = req.param('password');
+ if ( !pass || pass === "" || !isValidPassword( pass ) ) {
+ res.json({
+ status: 'error',
+ error: getPasswordProblem( pass )
+ });
+ return;
+ }
+
+ var spawn = require('child_process').spawn;
+ var err=0;
+ //device_settings.device_name = devicename;
+ var erroutput = "";
+ var output = "";
+ //var setpipass = process.cwd() + '/sudo_scripts/setpipass';
+ //var setpass = spawn( '/usr/bin/sudo', [setpipass] );
+ //setpass.stdout.on( 'data', function( d ) {
+ // output += d;
+ //});
+ //setpass.stderr.on( 'data', function( d ) {
+ // erroutput += d;
+ //});
+
+ //setpass.addListener( 'exit', function( code, signal ) {
+ var completed = function( code, signal ) {
+ err = code;
+
+
+ if ( err ) {
+ res.json({
+ status: "error",
+ error: erroutput
+ });
+ return;
+ }
+
+ //TODO - Load hashed password
+ var s = bcrypt.genSaltSync(10);
+ var h = bcrypt.hashSync( pass, s );
+ util.log("PASSWORD INITIALIZED");
+ device_settings.password_hash = h;
+ err = saveDeviceSettings();
+
+ if ( !err ) {
+ res.json({
+ status: "success"
+ });
+ } else {
+ res.json({
+ status: "error",
+ error: "Could not save device settings."
+ });
+ }
+
+ };
+
+ completed();
+
+ //setpass.stdin.write(pass + '\n');
+ //setpass.stdin.write(pass + '\n');
+ //setpass.stdin.end();
+
+};
+
+
+
+exports.api_changepassword_handler = function( req, res ) {
+
+ //only allow this step if they are authenticated
+ if ( !exports.isAuthenticated(req) ) {
+ res.json({
+ status: "error",
+ error: "not authenticated"
+ });
+ return;
+ }
+
+ var oldpass = req.param('oldpassword');
+ var pass = req.param('password');
+
+ //Make sure old pass is set and matches
+ if ( typeof oldpass === 'undefined' || oldpass === ""
+ || !bcrypt.compareSync( oldpass, device_settings.password_hash ) ) {
+ res.json({
+ status: 'error',
+ error: "old password was incorrect"
+ });
+ return;
+ }
+
+ if ( !pass || pass === "" || !isValidPassword( pass ) ) {
+ res.json({
+ status: 'error',
+ error: getPasswordProblem( pass )
+ });
+ return;
+ }
+
+ var spawn = require('child_process').spawn;
+ var err=0;
+ //device_settings.device_name = devicename;
+ var erroutput = "";
+ var output = "";
+ var setpipass = process.cwd() + '/sudo_scripts/setpipass';
+ var setpass = spawn( '/usr/bin/sudo', [setpipass] );
+ setpass.stdout.on( 'data', function( d ) {
+ output += d;
+ });
+ setpass.stderr.on( 'data', function( d ) {
+ erroutput += d;
+ });
+
+ setpass.addListener( 'exit', function( code, signal ) {
+ err = code;
+
+
+ if ( err ) {
+ res.json({
+ status: "error",
+ error: erroutput
+ });
+ return;
+ }
+
+ //TODO - Load hashed password
+ var s = bcrypt.genSaltSync(10);
+ var h = bcrypt.hashSync( pass, s );
+ util.log("PASSWORD INITIALIZED");
+ device_settings.password_hash = h;
+ err = saveDeviceSettings();
+
+ if ( !err ) {
+ res.json({
+ status: "success"
+ });
+ } else {
+ res.json({
+ status: "error",
+ error: "Could not save device settings."
+ });
+ }
+
+ });
+ setpass.stdin.write(pass + '\n');
+ setpass.stdin.write(pass + '\n');
+ setpass.stdin.end();
+
+};
+
+
+exports.login_handler = function( req, res ) {
+ var tmplvars = {};
+ tmplvars['static_url'] = exports.settings.staticurl;
+ tmplvars['app_name'] = exports.settings.appname;
+ tmplvars['app_url'] = exports.settings.appurl;
+ tmplvars['device_name'] = exports.settings.device_name;
+ tmplvars['page_mode'] = "login";
+
+
+ //TODO - should this log you out automatically?
+ req.session.authenticated = false;
+ res.render( exports.settings.viewpath + '/index', tmplvars );
+};
+
+exports.logout_handler = function( req, res ) {
+ var tmplvars = {};
+ tmplvars['static_url'] = exports.settings.staticurl;
+ tmplvars['app_name'] = exports.settings.appname;
+ tmplvars['app_url'] = exports.settings.appurl;
+ tmplvars['device_name'] = exports.settings.device_name;
+ tmplvars['page_mode'] = "logout";
+
+ req.session.authenticated = false;
+ res.render( exports.settings.viewpath + '/index', tmplvars );
+};
+
+exports.api_login_handler = function( req, res ) {
+ if ( typeof req.body.password !== 'undefined' && req.body.password !== "" ) {
+ var authenticated = exports.authenticate( req, req.body.password );
+ if ( authenticated === true ) {
+ res.json( { status: 'success'} );
+ return;
+ }
+ }
+ res.json( {
+ status: 'error',
+ error: 'invalid password'
+ } );
+};
+exports.api_logout_handler = function( req, res ) {
+ req.session.authenticated = false;
+
+ res.json( { status: 'success'} );
+};
+*/
+
+var saveDeviceSettings = function() {
+ err = fs.writeFileSync( process.cwd() + "/device.json", JSON.stringify(device_settings, null, 4), 'utf8' );
+ return err;
+};
+
+var reloadDeviceSettings = function() {
+ var settings = {
+ password_hash: '',
+ device_name: '',
+ hostname: '',
+ coder_owner: '',
+ coder_color: ''
+ };
+
+ var loadedsettings = JSON.parse(fs.readFileSync( process.cwd() + "/device.json", 'utf-8' ));
+ settings.password_hash = ( typeof loadedsettings.password_hash !== 'undefined' && loadedsettings.password_hash !== '' ) ? loadedsettings.password_hash : settings.password_hash;
+ settings.device_name = ( typeof loadedsettings.device_name !== 'undefined' && loadedsettings.device_name !== '' ) ? loadedsettings.device_name : settings.device_name;
+ settings.hostname = ( typeof loadedsettings.hostname !== 'undefined' && loadedsettings.hostname !== '' ) ? loadedsettings.hostname : settings.hostname;
+ settings.coder_owner = ( typeof loadedsettings.coder_owner !== 'undefined' && loadedsettings.coder_owner !== '' ) ? loadedsettings.coder_owner : settings.coder_owner;
+ settings.coder_color = ( typeof loadedsettings.coder_color !== 'undefined' && loadedsettings.coder_color !== '' ) ? loadedsettings.coder_color : settings.coder_color;
+
+ device_settings = settings;
+}
+reloadDeviceSettings();
+
+
+var isValidDeviceName = function( name ) {
+ if ( !name || name === '' ) {
+ return false;
+ }
+ //starts with an ascii word char. can contain word char's spaces and '
+ if ( !name.match(/^[a-zA-Z0-9][\w ']*$/) ) {
+ return false;
+ }
+ //ends in an ascii word char
+ if ( !name.match(/[a-zA-Z0-9]$/) ) {
+ return false;
+ }
+ return true;
+};
+var hostnameFromDeviceName = function( name ) {
+ var hostname = name;
+ hostname = hostname.toLowerCase();
+ hostname = hostname.replace(/[^a-z0-9\- ]/g, '');
+ hostname = hostname.replace(/[\- ]+/g,'-');
+ return hostname;
+};
+
+/*
+var getPasswordProblem = function( pass ) {
+ if ( !pass || pass === '' ) {
+ return "the password is empty";
+ }
+ if ( pass.length < 6 ) {
+ return "the password should contain at least 6 characters";
+ }
+ if ( !pass.match(/[a-z]/) ||
+ !pass.match(/[A-Z0-9\-\_\.\,\;\:\'\"\[\]\{\}\!\@\#\$\%\^\&\*\(\)\\].*[A-Z0-9\-\_\.\,\;\:\'\"\[\]\{\}\!\@\#\$\%\^\&\*\(\)\\]/) ) {
+ return "your password must contain a lower case letter and at least two upper case letters or numbers";
+ }
+};
+
+var isValidPassword = function( pass ) {
+ if ( !pass || pass === '' ) {
+ return false;
+ }
+ //at least 6 characters
+ if ( pass.length < 6 ) {
+ return false;
+ }
+ //contains lower case
+ if ( !pass.match(/[a-z]/) ) {
+ return false;
+ }
+ //contains two upper case or numbers
+ if ( !pass.match(/[A-Z0-9\-\_\.\,\;\:\'\"\[\]\{\}\!\@\#\$\%\^\&\*\(\)\\].*[A-Z0-9\-\_\.\,\;\:\'\"\[\]\{\}\!\@\#\$\%\^\&\*\(\)\\]/) ) {
+ return false;
+ }
+ return true;
+};
+*/
+
+var isValidColor = function( color ) {
+ if ( !color || color === '' ) {
+ return false;
+ }
+ color = color.toLowerCase();
+ if ( !color.match(/^\#[a-f0-9]{6}$/) ) {
+ return false;
+ }
+ return true;
+}
+
+
+
+
diff --git a/coder-apps/common/localauth/app/meta.json b/coder-apps/common/localauth/app/meta.json
new file mode 100644
index 00000000..f0786758
--- /dev/null
+++ b/coder-apps/common/localauth/app/meta.json
@@ -0,0 +1,8 @@
+{
+ "created": "2014-04-28",
+ "modified": "2014-04-28",
+ "color": "#1abc9c",
+ "author": "Macon Pegram",
+ "name": "Auth",
+ "hidden": true
+}
\ No newline at end of file
diff --git a/coder-apps/common/localauth/static/css/index.css b/coder-apps/common/localauth/static/css/index.css
new file mode 100644
index 00000000..2a1c6d98
--- /dev/null
+++ b/coder-apps/common/localauth/static/css/index.css
@@ -0,0 +1,133 @@
+
+body {
+ background-color: #f1c40f;
+ color: #fff;
+}
+
+.form {
+ text-align: center;
+ width: 400px;
+ left: 50%;
+ margin-top: 60px;
+ margin-left: -200px;
+ position: relative;
+}
+
+#animation {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+}
+
+.centercontainer {
+ display: table;
+ height: 100%;
+ width: 100%;
+ position: absolute;
+ top:0px;
+ left:0px;
+}
+.center {
+ display: table-cell;
+ vertical-align: middle;
+}
+
+h1 {
+ font-size: 28px;
+ line-height: 28px;
+ font-weight: bold;
+ margin:0;
+ padding: 0 0 20px 0;
+}
+
+.instructions {
+ font-size: 21px;
+ line-height: 1.2em;
+ padding: 0 0 20px 0;
+}
+
+.formfield {
+ position: relative;
+ width: 400px;
+}
+.formfield.textinput .label {
+ position: absolute;
+ color: #999;
+ top:13px;
+ left:10px;
+ font-size: 16px;
+ line-height: 20px;
+ -webkit-font-smoothing: antialiased;
+}
+
+.formfield input[type=text], .formfield input[type=password] {
+ border: 2px solid transparent;
+ width: 376px;
+ padding: 10px 10px;
+ background-color: #fff;
+ color: #666;
+ height: 20px;
+ line-height: 16px;
+ font-size: 16px;
+ font-family: Arial, sans-serif;
+ border-radius: 3px;
+ -webkit-font-smoothing: antialiased;
+ margin-bottom: 24px;
+}
+
+.formfield input.error {
+ border-color: rgba(255,0,0,0.5);
+}
+
+.errormessage {
+ border: 2px solid transparent;
+ width: 376px;
+ padding: 10px 10px;
+ background-color: #e74c3c;
+ color: #ffffff;
+ min-height: 20px;
+ line-height: 20px;
+ font-size: 12px;
+ font-family: Arial, sans-serif;
+ border-radius: 3px;
+ -webkit-font-smoothing: antialiased;
+ margin-bottom: 24px;
+ text-transform: uppercase;
+}
+
+.submit {
+ font-size: 21px;
+ font-weight: bold;
+ line-height: 60px;
+ border-radius: 4px;
+ width:400px;
+ margin-bottom: 24px;
+ background-color: rgba(0,0,0,0.6);
+}
+.submit.disabled, .submit.disabled:hover {
+ background-color: rgba(0,0,0,0.2);
+}
+.submit:hover {
+ background-color: rgba(0,0,0,1);
+ cursor: pointer;
+}
+
+.cancel {
+ font-size: 21px;
+ font-weight: bold;
+ line-height: 60px;
+ border-radius: 4px;
+ width:400px;
+ margin-bottom: 24px;
+ background-color: rgba(0,0,0,0.6);
+}
+.cancel:hover {
+ background-color: rgba(0,0,0,1);
+ cursor: pointer;
+}
+
+
+
+
+
+
diff --git a/coder-apps/common/localauth/static/js/index.js b/coder-apps/common/localauth/static/js/index.js
new file mode 100644
index 00000000..bbf0e62a
--- /dev/null
+++ b/coder-apps/common/localauth/static/js/index.js
@@ -0,0 +1,487 @@
+/**
+ * Coder for Raspberry Pi
+ * A simple platform for experimenting with web stuff.
+ * http://goo.gl/coder
+ *
+ * Copyright 2013 Google Inc. All Rights Reserved.
+ *
+ * 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.
+ */
+
+$(document).ready( function() {
+
+/*
+ if ( pagemode === "logout" ) {
+ setupLogoutFields();
+ $('#logout_form').show();
+ } else if ( pagemode === "login" ) {
+ setupLoginFields();
+ $('#login_form').show();
+ } else
+*/
+ if ( pagemode === "configure" ) {
+ setupConfigureFields();
+ $('#configure_form').show();
+ }
+/*
+ else if ( pagemode === "addpassword" ) {
+ setupAddPasswordFields();
+ $('#addpassword_form').show();
+ } else if ( pagemode === "changepassword" ) {
+ setupChangePasswordFields();
+ $('#changepassword_form').show();
+ } else {
+ setupLoginFields();
+ $('#login_form').show();
+ }
+*/
+ buildAnimation();
+ updateAnimation();
+ $(window).on('resize', function() {
+ setTimeout( buildAnimation, 1 );
+ });
+});
+
+var hideTextLabel = function() {
+ $(this).parent().find('.label').hide();
+};
+var focusTextInput = function() {
+ $(this).parent().find('input').focus();
+};
+var onBlurTextInput = function() {
+ if ( $(this).val() == "" ) {
+ $(this).parent().find('.label').show();
+ }
+};
+
+/*
+var testLoginSubmitEnable = function() {
+ var $form = $('#login_form');
+ if ( pagemode === "logout" ) {
+ $form = $('#logout_form');
+ }
+
+
+ if ( $form.find('.pass').val() && $form.find('.pass').val() != "" ) {
+ $form.find('.submit').removeClass('disabled');
+ } else {
+ $form.find('.submit').addClass('disabled');
+ }
+};
+*/
+
+var testConfigureSubmitEnable = function() {
+ var $form = $('#configure_form');
+ var devicename = $form.find('.device_name').val();
+ if ( devicename && devicename != "" &&
+ isValidDeviceName(devicename) ) {
+ $form.find('.submit').removeClass('disabled');
+ } else {
+ $form.find('.submit').addClass('disabled');
+ }
+};
+
+/*
+var testAddPasswordSubmitEnable = function() {
+ var $form = $('#addpassword_form');
+ var pass = $form.find('.pass').val();
+ var pass_repeat = $form.find('.pass_repeat').val();
+ $form.find('.pass, .pass_repeat').removeClass('error');
+ $form.find('.errormessage').css('visibility','hidden');
+
+ if ( isValidPassword(pass) && pass === pass_repeat ) {
+ $form.find('.submit').removeClass('disabled');
+ } else {
+ $form.find('.submit').addClass('disabled');
+ }
+};
+
+var testChangePasswordSubmitEnable = function() {
+ var $form = $('#changepassword_form');
+ var oldpass = $form.find('.oldpass').val();
+ var pass = $form.find('.pass').val();
+ var pass_repeat = $form.find('.pass_repeat').val();
+ $form.find('.errormessage').css('visibility','hidden');
+ $form.find('.oldpass, .pass, .pass_repeat').removeClass('error');
+ if ( oldpass !== "" && isValidPassword(pass) && pass === pass_repeat ) {
+ $form.find('.submit').removeClass('disabled');
+ } else {
+ $form.find('.submit').addClass('disabled');
+ }
+};
+
+
+var setupLoginFields = function() {
+ $('#login_form .formfield.textinput .label').click( focusTextInput );
+ $('#login_form .formfield.textinput input').click( focusTextInput );
+ $('#login_form .formfield.textinput input').focus( hideTextLabel );
+ $('#login_form .formfield.textinput input').blur( onBlurTextInput );
+ $('#login_form .formfield.textinput input').change( testLoginSubmitEnable );
+ $('#login_form .formfield.textinput input').keydown( function(e) {
+ if (e.which != 13) {
+ setTimeout( testLoginSubmitEnable, 0 );
+ }
+ });
+
+ //submit on enter or button click
+ $('#login_form .formfield.textinput .pass').keypress(function (e) {
+ if (e.which == 13) {
+ e.preventDefault();
+ loginClick( $('#login_form .formfield.textinput .pass') );
+ }
+ });
+ $('#login_form .submit').click( function() {
+ loginClick( $('#login_form .formfield.textinput .pass') );
+ });
+};
+var setupLogoutFields = function() {
+ $('#logout_form .formfield.textinput .label').click( focusTextInput );
+ $('#logout_form .formfield.textinput input').click( focusTextInput );
+ $('#logout_form .formfield.textinput input').focus( hideTextLabel );
+ $('#logout_form .formfield.textinput input').blur( onBlurTextInput );
+ $('#logout_form .formfield.textinput input').change( testLoginSubmitEnable );
+ $('#logout_form .formfield.textinput input').keydown( function(e) {
+ if (e.which != 13) {
+ setTimeout( testLoginSubmitEnable, 0 );
+ }
+ });
+
+ //submit on enter or button click
+ $('#logout_form .formfield.textinput .pass').keypress(function (e) {
+ if (e.which == 13) {
+ e.preventDefault();
+ loginClick( $('#logout_form .formfield.textinput .pass') );
+ }
+ });
+ $('#logout_form .submit').click( function() {
+ loginClick( $('#logout_form .formfield.textinput .pass') );
+ });
+};
+*/
+var setupConfigureFields = function() {
+ $('#configure_form .formfield.textinput .label').click( focusTextInput );
+ $('#configure_form .formfield.textinput input').click( focusTextInput );
+ $('#configure_form .formfield.textinput input').focus( hideTextLabel );
+ $('#configure_form .formfield.textinput input').blur( onBlurTextInput );
+ $('#configure_form .formfield.textinput input').change( testConfigureSubmitEnable );
+ $('#configure_form .formfield.textinput input').keydown( function(e) {
+ if (e.which != 13) {
+ setTimeout( testConfigureSubmitEnable, 0 );
+ }
+ });
+ $('#configure_form .submit').click( configureClick );
+ $('#configure_form .device_name').val( 'My Coder' ).parent().find('.label').hide();
+ testConfigureSubmitEnable();
+};
+/*
+var setupAddPasswordFields = function() {
+ $('#addpassword_form .formfield.textinput .label').click( focusTextInput );
+ $('#addpassword_form .formfield.textinput input').click( focusTextInput );
+ $('#addpassword_form .formfield.textinput input').focus( hideTextLabel );
+ $('#addpassword_form .formfield.textinput input').blur( onBlurTextInput );
+ $('#addpassword_form .formfield.textinput input').change( testAddPasswordSubmitEnable );
+ $('#addpassword_form .formfield.textinput input').keydown( function(e) {
+ if (e.which != 13) {
+ setTimeout( testAddPasswordSubmitEnable, 0 );
+ }
+ });
+
+ $('#addpassword_form .formfield.textinput .pass_repeat').keypress(function (e) {
+ if (e.which == 13) {
+ e.preventDefault();
+ addPasswordClick();
+ }
+ });
+
+ $('#addpassword_form .submit').click( addPasswordClick );
+};
+
+var setupChangePasswordFields = function() {
+ $('#changepassword_form .formfield.textinput .label').click( focusTextInput );
+ $('#changepassword_form .formfield.textinput input').click( focusTextInput );
+ $('#changepassword_form .formfield.textinput input').focus( hideTextLabel );
+ $('#changepassword_form .formfield.textinput input').blur( onBlurTextInput );
+ $('#changepassword_form .formfield.textinput input').change( testChangePasswordSubmitEnable );
+ $('#changepassword_form .formfield.textinput input').keydown( function(e) {
+ if (e.which != 13) {
+ setTimeout( testChangePasswordSubmitEnable, 0 );
+ }
+ });
+
+ $('#changepassword_form .formfield.textinput .pass_repeat').keypress(function (e) {
+ if (e.which == 13) {
+ e.preventDefault();
+ changePasswordClick();
+ }
+ });
+
+ $('#changepassword_form .submit').click( changePasswordClick );
+ $('#changepassword_form .cancel').click( function() {
+ window.location.href="http://github.com/";
+ });
+};
+
+var loginClick = function( what ) {
+ $this = $(what);
+ var $form = $('#login_form');
+ if ( pagemode === "logout" ) {
+ $form = $('#logout_form');
+ }
+
+ $form.find('.errormessage').css('visibility','hidden');
+ $form.find('.pass').removeClass('error');
+ $.post(
+ appurl + '/api/login',
+ {
+ password: $this.parent().find('.pass').val()
+ },
+ function( data ) {
+ console.log( data );
+ if( data.status === "success" ) {
+ var firstuse = "";
+ if ( typeof getParams['firstuse'] !== 'undefined' ) {
+ firstuse = '?firstuse';
+ }
+ window.location.href="http://github.com/app/coder" + firstuse;
+ } else {
+ $form.find('.errormessage').text( data.error ).css('visibility','visible');
+ $form.find('.pass').addClass('error');
+ }
+ }
+ );
+};
+*/
+var configureClick = function() {
+ $this = $(this);
+ var $form = $('#configure_form');
+
+ $form.find('.device_name').removeClass('error');
+ var devicename = $this.parent().find('.device_name').val();
+
+ if ( !isValidDeviceName(devicename) ) {
+ $form.find('.device_name').addClass('error');
+ return;
+ }
+
+ $.post(
+ appurl + '/api/devicename/set',
+ {
+ device_name: devicename
+ },
+ function( data ) {
+ console.log( data );
+ if( data.status === "success" ) {
+ window.location.href="http://github.com/app/auth";
+ } else {
+ $form.find('.device_name').addClass('error');
+ }
+ }
+ );
+};
+
+/*
+var addPasswordClick = function() {
+ var $form = $('#addpassword_form');
+
+ $form.find('.pass, .pass_repeat').removeClass('error');
+ $form.find('.errormessage').css('visibility','hidden');
+ var pass = $form.find('.pass').val();
+ var pass_repeat = $form.find('.pass_repeat').val();
+
+ if ( !isValidPassword(pass) ) {
+ $form.find('.pass').addClass('error');
+ $form.find('.errormessage').text( getPasswordProblem(pass) ).css('visibility','visible');
+ return;
+ }
+ if ( pass !== pass_repeat ) {
+ $form.find('.pass_repeat').addClass('error');
+ $form.find('.errormessage').text( "new password does not match" ).css('visibility','visible');
+ return;
+ }
+
+ $.post(
+ appurl + '/api/addpassword',
+ {
+ password: pass
+ },
+ function( data ) {
+ console.log( data );
+ if( data.status === "success" ) {
+ var firstuse = '';
+ if ( typeof getParams['firstuse'] !== 'undefined' ) {
+ firstuse = '?firstuse';
+ }
+ window.location.href="http://github.com/app/auth" + firstuse;
+ } else {
+ $form.find('.pass').addClass('error');
+ $form.find('.errormessage').text( data.error ).css('visibility','visible');
+ }
+ }
+ );
+};
+
+
+var changePasswordClick = function() {
+
+ var $form = $('#changepassword_form');
+
+ $form.find('.oldpass, .pass, .pass_repeat').removeClass('error');
+ $form.find('.errormessage').css('visibility','hidden');
+ var oldpass = $form.find('.oldpass').val();
+ var pass = $form.find('.pass').val();
+ var pass_repeat = $form.find('.pass_repeat').val();
+
+ if ( oldpass === "" ) {
+ $form.find('.oldpass').addClass('error');
+ $form.find('.errormessage').text( "your current password is required" ).css('visibility','visible');
+ return;
+ }
+ if ( !isValidPassword(pass) ) {
+ $form.find('.pass').addClass('error');
+ $form.find('.errormessage').text( getPasswordProblem(pass) ).css('visibility','visible');
+ return;
+ }
+ if ( pass !== pass_repeat ) {
+ $form.find('.pass_repeat').addClass('error');
+ $form.find('.errormessage').text( "new password does not match" ).css('visibility','visible');
+ return;
+ }
+
+ $.post(
+ appurl + '/api/changepassword',
+ {
+ oldpassword: oldpass,
+ password: pass
+ },
+ function( data ) {
+ console.log( data );
+ if( data.status === "success" ) {
+ window.location.href="http://github.com/app/auth";
+ } else {
+ $form.find('.oldpass').addClass('error');
+ $form.find('.errormessage').text( data.error ).css('visibility','visible');
+ }
+ }
+ );
+};
+*/
+var isValidDeviceName = function( name ) {
+ if ( !name || name === '' ) {
+ return false;
+ }
+ //starts with an ascii word char. can contain word char's spaces and '
+ if ( !name.match(/^[a-zA-Z0-9][\w ']*$/) ) {
+ return false;
+ }
+ //ends in an ascii word char
+ if ( !name.match(/[a-zA-Z0-9]$/) ) {
+ return false;
+ }
+ return true;
+};
+
+/*
+var getPasswordProblem = function( pass ) {
+ if ( !pass || pass === '' ) {
+ return "the password is empty";
+ }
+ if ( pass.length < 6 ) {
+ return "the password should contain at least 6 characters";
+ }
+ if ( !pass.match(/[a-z]/) ||
+ !pass.match(/[A-Z0-9\-\_\.\,\;\:\'\"\[\]\{\}\!\@\#\$\%\^\&\*\(\)\\].*[A-Z0-9\-\_\.\,\;\:\'\"\[\]\{\}\!\@\#\$\%\^\&\*\(\)\\]/) ) {
+ return "your password must contain a lower case letter and at least two upper case letters or numbers";
+ }
+};
+var isValidPassword = function( pass ) {
+ if ( !pass || pass === '' ) {
+ return false;
+ }
+ //at least 6 characters
+ if ( pass.length < 6 ) {
+ return false;
+ }
+ //contains lower case
+ if ( !pass.match(/[a-z]/) ) {
+ return false;
+ }
+ //contains two upper case or numbers
+ if ( !pass.match(/[A-Z0-9\-\_\.\,\;\:\'\"\[\]\{\}\!\@\#\$\%\^\&\*\(\)\\].*[A-Z0-9\-\_\.\,\;\:\'\"\[\]\{\}\!\@\#\$\%\^\&\*\(\)\\]/) ) {
+ return false;
+ }
+ return true;
+};
+*/
+var circles = [];
+var $canvas;
+var ctx;
+var buildAnimation = function() {
+ circles = [];
+ $canvas = $("#animation");
+ ctx = $canvas.get(0).getContext("2d");
+ var w = $canvas.parent().width();
+ var h = $canvas.parent().height();
+ $canvas.attr('width', w);
+ $canvas.attr('height', h);
+
+ for ( var x=0; x<20; x++ ) {
+ var sx = (Math.random() * (w+100)) - 50;
+ var sy = (Math.random() * 800) - 400;
+ var circle = {
+ sx: sx,
+ sy: sy,
+ x: sx,
+ y: sy,
+ r: (Math.random() * 150) + 30,
+ opacity: .2,
+ direction: Math.random() > .5? 1:-1
+ };
+ circles.push( circle );
+ }
+};
+
+var updateAnimation = function() {
+ ctx.clearRect(0, 0, $canvas.width(), $canvas.height());
+ for ( var x=0; x .9 ) {
+ circle.opacity = .9;
+ }
+
+ circle.x += (Math.random() * .2) * circle.direction;
+ if ( circle.x < circle.sx - 250 ) {
+ circle.x = circle.sx - 250;
+ circle.direction=1;
+ } else if ( circle.x > circle.sx + 250 ) {
+ circle.x = circle.sx + 250;
+ circle.direction = -1;
+ }
+
+
+ }
+ setTimeout( updateAnimation, 1000/60 );
+};
+
+
+
diff --git a/coder-apps/common/localauth/static/media/.gitignore b/coder-apps/common/localauth/static/media/.gitignore
new file mode 100644
index 00000000..e69de29b
diff --git a/coder-apps/common/localauth/views/index.html b/coder-apps/common/localauth/views/index.html
new file mode 100644
index 00000000..bca909c6
--- /dev/null
+++ b/coder-apps/common/localauth/views/index.html
@@ -0,0 +1,109 @@
+
+
+
+ Coder
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{device_name}}
+
+
+
+
+
+
Welcome to Coder
+
First, let's give your Coder a name.
+
+ Your Coder's Name
+
+
+
OK. Save My Coder's Name
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/coder-apps/common/pop_up_penguins/app/app.js b/coder-apps/common/pop_up_penguins/app/app.js
new file mode 100644
index 00000000..5e949e05
--- /dev/null
+++ b/coder-apps/common/pop_up_penguins/app/app.js
@@ -0,0 +1,30 @@
+exports.settings={};
+//These are dynamically updated by the runtime
+//settings.appname - the app id (folder) where your app is installed
+//settings.viewpath - prefix to where your view html files are located
+//settings.staticurl - base url path to static assets /static/apps/appname
+//settings.appurl - base url path to this app /app/appname
+//settings.device_name - name given to this coder by the user, Ie."Billy's Coder"
+//settings.coder_owner - name of the user, Ie. "Suzie Q."
+//settings.coder_color - hex css color given to this coder.
+
+exports.get_routes = [
+ { path:'/', handler:'index_handler' },
+];
+
+exports.post_routes = [
+];
+
+
+exports.index_handler = function( req, res ) {
+ var tmplvars = {};
+ tmplvars['static_url'] = exports.settings.staticurl;
+ tmplvars['app_name'] = exports.settings.appname;
+ tmplvars['app_url'] = exports.settings.appurl;
+ tmplvars['device_name'] = exports.settings.device_name;
+
+ res.render( exports.settings.viewpath + '/index', tmplvars );
+};
+
+exports.on_destroy = function() {
+};
\ No newline at end of file
diff --git a/coder-apps/common/pop_up_penguins/app/meta.json b/coder-apps/common/pop_up_penguins/app/meta.json
new file mode 100644
index 00000000..9b118b5b
--- /dev/null
+++ b/coder-apps/common/pop_up_penguins/app/meta.json
@@ -0,0 +1,8 @@
+{
+ "created": "2013-12-17",
+ "modified": "2013-12-20",
+ "color": "#80d4ea",
+ "author": "Coder Team",
+ "name": "Pop-Up Penguins",
+ "hidden": false
+}
\ No newline at end of file
diff --git a/coder-apps/common/pop_up_penguins/static/css/index.css b/coder-apps/common/pop_up_penguins/static/css/index.css
new file mode 100644
index 00000000..7ed9d160
--- /dev/null
+++ b/coder-apps/common/pop_up_penguins/static/css/index.css
@@ -0,0 +1,186 @@
+
+/*This style gives the body element in our HTML a cool blue background color*/
+body {
+ background-color: #ccf5f5;
+}
+
+/*This styles the div in the HTML that contains and centers our penguin game*/
+#gameholder {
+ width: 600px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/*This styles the title div in or HTML*/
+#title {
+ width: 600px;
+ height: 150px;
+ /*This property embeds an image into the background of our div*/
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/penguin_title.png');
+}
+
+/*This pseudo class is applied when the mouse hovers over the 1st penguin div*/
+.penguin1:hover {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_1_hover.png');
+ /*This property changes our cursor to a pointer indicating an interactive element*/
+ cursor: pointer;
+}
+
+/*This pseudo class is applied when our 1st penguin div is clicked by the mouse button*/
+.penguin1:active {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/penguin_1.png');
+}
+
+/*This is the style for our 1st penguin div*/
+.penguin1 {
+ width: 200px;
+ height: 200px;
+ float: left;
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_1.png');
+}
+
+/*These are the styles for our 2nd penguin div.*/
+
+.penguin2:hover {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_2_hover.png');
+ cursor: pointer;
+}
+
+.penguin2:active {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/penguin_2.png');
+}
+
+.penguin2 {
+ width: 200px;
+ height: 200px;
+ float: left;
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_2.png');
+}
+
+/*These are the styles for our 3rd penguin div.*/
+
+.penguin3:hover {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_3_hover.png');
+ cursor: pointer;
+}
+
+.penguin3:active {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/penguin_3.png');
+}
+
+.penguin3 {
+ width: 200px;
+ height: 200px;
+ float: left;
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_3.png');
+}
+
+/*These are the styles for our 4th penguin div.*/
+
+.penguin4:hover {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_4_hover.png');
+ cursor: pointer;
+}
+
+.penguin4:active {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/penguin_4.png');
+}
+
+.penguin4 {
+ width: 200px;
+ height: 200px;
+ float: left;
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_4.png');
+}
+
+/*These are the styles for our 5th penguin div.*/
+
+.penguin5:hover {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_5_hover.png');
+ cursor: pointer;
+}
+
+.penguin5:active {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/penguin_5.png');
+}
+
+.penguin5 {
+ width: 200px;
+ height: 200px;
+ float: left;
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_5.png');
+}
+
+/*These are the styles for our 6th penguin div.*/
+
+.penguin6:hover {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_6_hover.png');
+ cursor: pointer;
+}
+
+.penguin6:active {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/penguin_6.png');
+}
+
+.penguin6 {
+ width: 200px;
+ height: 200px;
+ float: left;
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_6.png');
+}
+
+/*These are the styles for our 7th penguin div.*/
+
+.penguin7:hover {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_7_hover.png');
+ cursor: pointer;
+}
+
+.penguin7:active {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/penguin_7.png');
+}
+
+.penguin7 {
+ width: 200px;
+ height: 200px;
+ float: left;
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_7.png');
+}
+
+/*These are the styles for our 8th penguin div.*/
+
+.penguin8:hover {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_8_hover.png');
+ cursor: pointer;
+}
+
+.penguin8:active {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/penguin_8.png');
+}
+
+.penguin8 {
+ width: 200px;
+ height: 200px;
+ float: left;
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_8.png');
+}
+
+/*These are the styles for our yeti div.*/
+
+.yeti:hover {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_9_hover.png');
+ cursor: pointer;
+}
+
+.yeti:active {
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/yeti.png');
+}
+
+.yeti {
+ width: 200px;
+ height: 200px;
+ float: left;
+ background-image:url('http://github.com/static/apps/pop_up_penguins/media/mound_9.png');
+}
+
+
diff --git a/coder-apps/common/pop_up_penguins/static/js/index.js b/coder-apps/common/pop_up_penguins/static/js/index.js
new file mode 100644
index 00000000..45738ccd
--- /dev/null
+++ b/coder-apps/common/pop_up_penguins/static/js/index.js
@@ -0,0 +1,10 @@
+
+$(document).ready( function() {
+
+ //This code will run after your page loads
+
+ $(".yeti").mousedown(function() {
+ alert("Yaaaarrrr!");
+ });
+
+});
\ No newline at end of file
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_1.png b/coder-apps/common/pop_up_penguins/static/media/mound_1.png
new file mode 100644
index 00000000..5db6d3fc
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_1.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_1_hover.png b/coder-apps/common/pop_up_penguins/static/media/mound_1_hover.png
new file mode 100644
index 00000000..d87463e3
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_1_hover.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_2.png b/coder-apps/common/pop_up_penguins/static/media/mound_2.png
new file mode 100644
index 00000000..79b4eddb
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_2.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_2_hover.png b/coder-apps/common/pop_up_penguins/static/media/mound_2_hover.png
new file mode 100644
index 00000000..4235d7c1
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_2_hover.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_3.png b/coder-apps/common/pop_up_penguins/static/media/mound_3.png
new file mode 100644
index 00000000..f92d0ab3
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_3.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_3_hover.png b/coder-apps/common/pop_up_penguins/static/media/mound_3_hover.png
new file mode 100644
index 00000000..970ca6e9
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_3_hover.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_4.png b/coder-apps/common/pop_up_penguins/static/media/mound_4.png
new file mode 100644
index 00000000..25a1c2fe
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_4.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_4_hover.png b/coder-apps/common/pop_up_penguins/static/media/mound_4_hover.png
new file mode 100644
index 00000000..5c7a8274
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_4_hover.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_5.png b/coder-apps/common/pop_up_penguins/static/media/mound_5.png
new file mode 100644
index 00000000..4e3c7a78
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_5.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_5_hover.png b/coder-apps/common/pop_up_penguins/static/media/mound_5_hover.png
new file mode 100644
index 00000000..95cc58fb
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_5_hover.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_6.png b/coder-apps/common/pop_up_penguins/static/media/mound_6.png
new file mode 100644
index 00000000..baf6a405
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_6.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_6_hover.png b/coder-apps/common/pop_up_penguins/static/media/mound_6_hover.png
new file mode 100644
index 00000000..339f97b7
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_6_hover.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_7.png b/coder-apps/common/pop_up_penguins/static/media/mound_7.png
new file mode 100644
index 00000000..590ce635
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_7.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_7_hover.png b/coder-apps/common/pop_up_penguins/static/media/mound_7_hover.png
new file mode 100644
index 00000000..437654a0
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_7_hover.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_8.png b/coder-apps/common/pop_up_penguins/static/media/mound_8.png
new file mode 100644
index 00000000..c16664fd
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_8.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_8_hover.png b/coder-apps/common/pop_up_penguins/static/media/mound_8_hover.png
new file mode 100644
index 00000000..9cdd0bb3
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_8_hover.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_9.png b/coder-apps/common/pop_up_penguins/static/media/mound_9.png
new file mode 100644
index 00000000..274a9eb6
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_9.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/mound_9_hover.png b/coder-apps/common/pop_up_penguins/static/media/mound_9_hover.png
new file mode 100644
index 00000000..81fad57d
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/mound_9_hover.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/penguin_1.png b/coder-apps/common/pop_up_penguins/static/media/penguin_1.png
new file mode 100644
index 00000000..433e6ae6
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/penguin_1.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/penguin_2.png b/coder-apps/common/pop_up_penguins/static/media/penguin_2.png
new file mode 100644
index 00000000..34ad06af
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/penguin_2.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/penguin_3.png b/coder-apps/common/pop_up_penguins/static/media/penguin_3.png
new file mode 100644
index 00000000..ce969cb5
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/penguin_3.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/penguin_4.png b/coder-apps/common/pop_up_penguins/static/media/penguin_4.png
new file mode 100644
index 00000000..0f47d461
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/penguin_4.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/penguin_5.png b/coder-apps/common/pop_up_penguins/static/media/penguin_5.png
new file mode 100644
index 00000000..3353affd
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/penguin_5.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/penguin_6.png b/coder-apps/common/pop_up_penguins/static/media/penguin_6.png
new file mode 100644
index 00000000..93854d77
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/penguin_6.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/penguin_7.png b/coder-apps/common/pop_up_penguins/static/media/penguin_7.png
new file mode 100644
index 00000000..c60eae51
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/penguin_7.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/penguin_8.png b/coder-apps/common/pop_up_penguins/static/media/penguin_8.png
new file mode 100644
index 00000000..fc36c335
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/penguin_8.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/penguin_title.png b/coder-apps/common/pop_up_penguins/static/media/penguin_title.png
new file mode 100644
index 00000000..7b29cfae
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/penguin_title.png differ
diff --git a/coder-apps/common/pop_up_penguins/static/media/yeti.png b/coder-apps/common/pop_up_penguins/static/media/yeti.png
new file mode 100644
index 00000000..4110d78d
Binary files /dev/null and b/coder-apps/common/pop_up_penguins/static/media/yeti.png differ
diff --git a/coder-apps/common/pop_up_penguins/views/index.html b/coder-apps/common/pop_up_penguins/views/index.html
new file mode 100644
index 00000000..fad1107b
--- /dev/null
+++ b/coder-apps/common/pop_up_penguins/views/index.html
@@ -0,0 +1,43 @@
+
+
+
+ Coder
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/coder-apps/common/rva_coderdojo_matrix/app/app.js b/coder-apps/common/rva_coderdojo_matrix/app/app.js
new file mode 100644
index 00000000..eb6c824e
--- /dev/null
+++ b/coder-apps/common/rva_coderdojo_matrix/app/app.js
@@ -0,0 +1,65 @@
+/*
+
+Hello Coder!
+
+This is the Node.JS piece of your program. Unlike HTML, CSS, and JS,
+this part of your program doesn't run in your web browser. Instead
+it runs on your Raspberry Pi, and can do more advanced things like
+save and retrieve data. Coders call this "back end" or "server side"
+software, and HTML, JS, and CSS code is "front end" or "client side"
+software.
+
+There are a ton of different languages for writing server side software,
+but Coder's built to use one system, called Node.JS. Node.JS uses
+the Javascript language for making server-side code. Because it's
+Javascript, when you get to writing back end code, you'll find that
+it's very similar to what you've learned in front end JS.
+
+
+WHAT'S GOING ON HERE
+This program contains just the default back end code. The index_handler
+function in this program is used to send your HTML code from the
+server to your web browser. That's it!
+
+Many demos in Coder look just like this in the Node.js file. To do
+front end coding, you won't need to do a thing in Node. It's always
+here, though, for when you get to making more advanced things.
+
+If you're new to Coder, don't bother changing anything in here. Yet...
+
+*/
+
+
+exports.settings={};
+//These are dynamically updated by the runtime
+//settings.appname - the app id (folder) where your app is installed
+//settings.viewpath - prefix to where your view html files are located
+//settings.staticurl - base url path to static assets /static/apps/appname
+//settings.appurl - base url path to this app /app/appname
+//settings.device_name - name given to this coder by the user, Ie."Billy's Coder"
+//settings.coder_owner - name of the user, Ie. "Suzie Q."
+//settings.coder_color - hex css color given to this coder.
+
+exports.get_routes = [
+ { path:'/', handler:'index_handler' },
+];
+
+exports.post_routes = [
+];
+
+
+exports.index_handler = function( req, res ) {
+ var tmplvars = {};
+ tmplvars['static_url'] = exports.settings.staticurl;
+ tmplvars['app_name'] = exports.settings.appname;
+ tmplvars['app_url'] = exports.settings.appurl;
+ tmplvars['device_name'] = exports.settings.device_name;
+
+ res.render( exports.settings.viewpath + '/index', tmplvars );
+};
+
+exports.on_destroy = function() {
+};
+
+
+
diff --git a/coder-apps/common/rva_coderdojo_matrix/app/meta.json b/coder-apps/common/rva_coderdojo_matrix/app/meta.json
new file mode 100644
index 00000000..9613c6c9
--- /dev/null
+++ b/coder-apps/common/rva_coderdojo_matrix/app/meta.json
@@ -0,0 +1,8 @@
+{
+ "created": "2013-05-08",
+ "modified": "2014-01-14",
+ "color": "#00cc00",
+ "author": "Macon Pegram",
+ "name": "RVA Coder Dojo",
+ "hidden": false
+}
\ No newline at end of file
diff --git a/coder-apps/common/rva_coderdojo_matrix/static/css/index.css b/coder-apps/common/rva_coderdojo_matrix/static/css/index.css
new file mode 100644
index 00000000..03177c00
--- /dev/null
+++ b/coder-apps/common/rva_coderdojo_matrix/static/css/index.css
@@ -0,0 +1,37 @@
+body {
+ margin: 0px;
+ background-color: #808080;
+}
+
+/* This helps to center the matrix canvas in the middle of the screen. */
+#matrix-canvas-stack canvas {
+ margin: auto; /* Tells the browser to automatically size the margins */
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ width: 90%; /* Scale the canvas larger or smaller as the browser window scales */
+ max-width: 800px; /* Doesn't allows the canvas become greater than 800 pixels wide */
+}
+
+/* This style is used when the browser cannot do HTML 5 canvas */
+/* It styles how the non-animated image should be positioned */
+#matrix-fallback {
+ margin: auto;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ width: 90%;
+ max-width: 800px;
+}
+
+/* This is a color hack to make the Google Coder controls more prominent */
+#coder_basicnav {
+ color: rgba(238,238,0,.8);
+ background-color: rgba(0, 255, 0, .4);
+}
+#coder_basicnav .coder_home a {
+ color: rgba(238,238,0,.8);
+}
+/* End of color hack */
\ No newline at end of file
diff --git a/coder-apps/common/rva_coderdojo_matrix/static/js/index.js b/coder-apps/common/rva_coderdojo_matrix/static/js/index.js
new file mode 100644
index 00000000..3c4e85f8
--- /dev/null
+++ b/coder-apps/common/rva_coderdojo_matrix/static/js/index.js
@@ -0,0 +1,253 @@
+// The page must be loaded first before the script can run.
+// This makes sure that the page is loaded first.
+$(document).ready(function () {
+ // Check to see if the browser is capable of showing an HTML 5 canvas.
+ // If so we can do matrix rain!
+ if (!!window.HTMLCanvasElement) {
+ var matrixRain = new MatrixRain(); // Create a Javascript Object of MatrixRain
+ matrixRain.setImageURL(staticurl + '/media/rva-coderdojo-lg.png');
+
+ // Try changing this to see what happens!
+ matrixRain.setText("WELCOME TO THE RVA CODER DOJO");
+
+ // I wonder what happens if you change this number?
+ matrixRain.setFontSize(10);
+
+ // Too fast or too slow? You are in control.
+ // Try changing this value.
+ matrixRain.setDelay(80);
+
+ // Try different colors.. remember:
+ // - rgb(redColor, greenColor, blueColor)
+ // - Colors can go from 0 [dark] to 255 [bright]
+ // - So rgb(255, 0, 255) will be bright purple (red + blue).
+ // rgb(0, 255, 0) = Bright green
+ matrixRain.setFontColor('rgb(0, 255, 0)');
+
+ // Start running the matrix rain animation!
+ matrixRain.start();
+ }
+ else
+ {
+ // If we can't add an HTML 5 canvas, just show the coder dojo non-animated image.
+ // Add a fallback image if canvas is not supported.
+ var fallback = document.createElement('img'); // Creates a regular tag.
+ fallback.id='matrix-fallback';
+ fallback.src = staticurl + '/media/rva-coderdojo-nocanvas-lg.png';
+ // Attaches the RVA coder dojo image to the page as a regular html
+ document.body.appendChild(fallback);
+ }
+});
+
+// Original matrix-rain code from: http://thecodeplayer.com/walkthrough/matrix-rain-animation-html5-canvas-javascript
+// (refactored and enhanced by RVA Coder Dojo)
+
+// We are using a Javascript style of coding to create a call called "MatrixRain" here.
+function MatrixRain (containerId) {
+
+ // This will insert a
into the page and give it the id 'matrix-canvas-stack'
+ this.canvasStackDiv = document.createElement('div');
+ this.canvasStackDiv.id='matrix-canvas-stack';
+
+ // If the user of this class passes in a containerId value, then we will place the
+ // 'matrix-canvas-stack' inside the provided container element in the page.
+ // this lets the user position the matrix rain logo wherever they want on the page.
+ if (containerId != null && document.getElementById(containerId) != null) {
+ this.container = document.getElementById(containerId);
+ this.container.appendChild(this.canvasStackDiv);
+ }
+ else // If not, we just drop the 'matrix-canvas-stack' as the last element in the page.
+ document.body.appendChild(this.canvasStackDiv);
+
+ // These next values are defaults which can be changed using
+ // the set method defined later on.
+
+ // this is the default speed for the rain.
+ this.renderDelay = 50;
+
+ // This will be the URL for the overlay image (if you have one) default is no image.
+ this.imageURL = null;
+
+ // This can be a link to jump to if the user clicks on the logo. default is none
+ this.gotoURL = null;
+
+ // These are just random looking matrix characters by default. They can be changed.
+ this.matrixText = '田由甲申甴电甶男甸甹町画甼甽甾甿畀畁畂畃畄畅畆畇畈畉畊畋界畍畎畏畐畑';
+
+ // This is the default font size for the matrix rain letters.
+ this.fontSize=8;
+
+ // This is the default color (bright green)
+ this.fontColor='rgb(0,255,0)';
+};
+
+// Call setGotoURL to create a landing web page to go to when the user clicks on the rain.
+MatrixRain.prototype.setGotoURL = function(gotoURL) {
+ if (gotoURL != null) {
+ this.gotoURL = gotoURL;
+ }
+};
+
+// Call setText to put your own secret message in the rain.
+MatrixRain.prototype.setText = function (textIn) {
+ if (textIn != null)
+ this.matrixText = textIn;
+};
+
+// Call setDelay with a number. Bigger numbers make the rain animate slower.
+// Smaller numbers make it go faster.
+MatrixRain.prototype.setDelay = function (delayIn) {
+ if (delayIn != null && delayIn >= 0)
+ this.renderDelay = delayIn;
+};
+
+// Call setImageURL to create an overlay image for the rain. Only the transparent portions of
+// the image will show the rain.
+MatrixRain.prototype.setImageURL = function (imageURL) {
+ if (imageURL != undefined)
+ this.imageURL = imageURL;
+};
+
+// Call setFontSize to change how big or small the letters are that make up your secret message.
+MatrixRain.prototype.setFontSize = function (fontSize) {
+ if (fontSize != undefined && fontSize > 0)
+ this.fontSize = fontSize;
+};
+
+// Call setFontColor to change what color the text is in your secret message.
+MatrixRain.prototype.setFontColor = function (fontColor) {
+ if (fontColor != undefined)
+ this.fontColor = fontColor;
+};
+
+// This helps to resize the canvas.
+MatrixRain.prototype.resize = function (width, height) {
+ this.matrixCanvas.width=width;
+ this.matrixCanvas.height= height;
+ if (this.matrixOverlay != undefined) {
+ this.matrixOverlay.width=width;
+ this.matrixOverlay.height=height;
+ }
+};
+
+// This creates an HTML 5 canvas element with the provided name (id), height and width.
+MatrixRain.prototype.createCanvas = function (id, width, height) {
+ var newCanvas = document.createElement('canvas');
+ newCanvas.id = id;
+ if (width != null)
+ newCanvas.width = width;
+ if (height != null)
+ newCanvas.height = height;
+
+ // sets the alignment so the layers will overlap.
+ newCanvas.style.position="absolute";
+ newCanvas.style.backgroundColor="transparent";
+
+ return newCanvas;
+};
+
+// This is the function to start the matrix rain animation. Call it
+// after you have created a MatrixRain javascript object, and called
+// all the setters you want to configure your rain. For example:
+// var matrixRain = new MatrixRain();
+// matrixRain.setText('Hello World');
+// matrixRain.setFontSize(12);
+// matrixRain.start();
+MatrixRain.prototype.start = function() {
+ this.matrixCanvas = this.createCanvas('matrix-canvas');
+ this.matrixCanvas.style.zIndex=0;
+ this.matrixCanvas.style.background="black";
+ this.canvasStackDiv.appendChild(this.matrixCanvas);
+ if (this.imageURL != null) {
+ img = new Image();
+ img.matrix = this;
+ img.onerror = function() {
+ this.matrix.startRain();
+ window.console && console.log('Could not load image: ' + this.imageURL);
+ };
+ img.onload = function() {
+ this.matrix.addOverlay(img);
+ this.matrix.startRain();
+ };
+ img.src = this.imageURL;
+ }
+ else {
+ this.startRain();
+ }
+};
+
+// This function adds the overlay image as an HTML 5 canvas on top of the rain animation
+// It is sized to be the same height and width as the rain animation.
+MatrixRain.prototype.addOverlay = function(image) {
+ this.matrixOverlay = this.createCanvas('matrix-overlay', image.width, image.height);
+ this.matrixOverlay.style.zIndex=10;
+ this.canvasStackDiv.appendChild(this.matrixOverlay);
+ this.resize(image.width, image.height);
+ var context = this.matrixOverlay.getContext('2d');
+ context.drawImage(image, 0,0);
+};
+
+// This will do the initial setup for the matrix rain data.
+MatrixRain.prototype.startRain = function() {
+ this.mtxCtx = this.matrixCanvas.getContext('2d');
+
+ this.matrixText = this.matrixText.split("");
+
+ // How many characters across the canvas
+ this.columns = this.matrixCanvas.width / this.fontSize;
+ this.drops = [];
+
+ // Where to start in the matrix code
+ // This allows for readable text messages to rain down.
+ this.textIndex = [];
+
+ for (var x = 0; x < this.columns; x++) {
+ this.drops[x] = this.matrixCanvas.height + 1;
+ this.textIndex[x] = Math.floor(Math.random() * this.matrixText.length);
+ }
+
+ // this is a poor hack to attach to the global namespace for the callback
+ window.MatrixRainInstance = this;
+
+ // This sets a timer to wait until "renderDelay" expires
+ // then call the "render()" function (defined below)
+ // this process repeats forever until the user leaves the page.
+ setInterval(this.render, this.renderDelay);
+};
+
+// This function is the animation for the matrix rain.
+MatrixRain.prototype.render = function() {
+
+ // This gets access to the matrix rain class
+ var _this = window.MatrixRainInstance;
+
+ // Fill the canvas with a very transparent black.
+ // This is what causes the letters to slowly fade out.
+ _this.mtxCtx.fillStyle="rgba(0,0,0,0.05)";
+ _this.mtxCtx.fillRect(0, 0, _this.matrixCanvas.width, _this.matrixCanvas.height);
+
+ // Now set the color and font size for the matrix text
+ _this.mtxCtx.fillStyle = _this.fontColor;
+ _this.mtxCtx.font = _this.fontSize + "px arial";
+
+ // Loop over each of the drops
+ for (var i=0; i < _this.drops.length; i++) {
+
+ // Start adding text to the canvas
+ _this.textIndex[i] = (_this.textIndex[i] + 1) % _this.matrixText.length;
+ var text = _this.matrixText[_this.textIndex[i]];
+
+ // x = i * fontSize, y = value of drops[i]*fontSize
+ _this.mtxCtx.fillText(text, i*_this.fontSize, _this.drops[i]*_this.fontSize);
+
+ // reset a drop after it hits the bottom and have it start to drop from the top.
+ if (_this.drops[i] * _this.fontSize > _this.matrixCanvas.height && Math.random() > 0.875) {
+ _this.drops[i] = 0;
+ }
+
+ // bump Y coordinate
+ _this.drops[i]++;
+ }
+};
+
+
diff --git a/coder-apps/common/rva_coderdojo_matrix/static/media/rva-coderdojo-lg.png b/coder-apps/common/rva_coderdojo_matrix/static/media/rva-coderdojo-lg.png
new file mode 100644
index 00000000..5e281824
Binary files /dev/null and b/coder-apps/common/rva_coderdojo_matrix/static/media/rva-coderdojo-lg.png differ
diff --git a/coder-apps/common/rva_coderdojo_matrix/static/media/rva-coderdojo-nocanvas-lg.png b/coder-apps/common/rva_coderdojo_matrix/static/media/rva-coderdojo-nocanvas-lg.png
new file mode 100644
index 00000000..cd1160d7
Binary files /dev/null and b/coder-apps/common/rva_coderdojo_matrix/static/media/rva-coderdojo-nocanvas-lg.png differ
diff --git a/coder-apps/common/rva_coderdojo_matrix/views/index.html b/coder-apps/common/rva_coderdojo_matrix/views/index.html
new file mode 100644
index 00000000..cbba670e
--- /dev/null
+++ b/coder-apps/common/rva_coderdojo_matrix/views/index.html
@@ -0,0 +1,31 @@
+
+
+
+
+ RVA Coders
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/coder-apps/install_all.cmd b/coder-apps/install_all.cmd
new file mode 100644
index 00000000..9ede89ea
--- /dev/null
+++ b/coder-apps/install_all.cmd
@@ -0,0 +1,21 @@
+@echo off
+if =%1-==-- echo "Usage: install_all.cmd [coderbase]" & exit /b
+
+set base=%1
+
+REM Create base folder
+IF NOT EXIST %base% (
+ mkdir %base%
+)
+
+echo "Copying base files...."
+xcopy ..\coder-base\*.* %base% /E
+
+echo "Installing common files"
+call install_common.cmd %base%
+
+echo "Preparing dependent libraries"
+cd %base%
+
+REM Note this isn't going to work because npm isn't installed
+REM node\npm install
\ No newline at end of file
diff --git a/coder-apps/install_all.sh b/coder-apps/install_all.sh
new file mode 100755
index 00000000..24049aa1
--- /dev/null
+++ b/coder-apps/install_all.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+##
+## Copies the common platform apps to
+## the coder-base working directory.
+##
+## sh install_all base_path
+##
+## Eg.
+## sh install_all ../coder-base/
+
+if [ $# != 1 ]
+ then
+ echo -e "\nUse:\ninstall_all coderbase\n"
+ exit
+fi
+
+base=$1
+
+## Setup the base installation folder.
+mkdir -p $base
+
+## Create basic install
+cp -r ../coder-base/* $base/
+./install_common.sh $base
+
+## Using the base folder's copy of npm perform the necessary library pulls.
+echo "Preparing dependent libraries"
+cd $base
+./node/bin/npm install
+
+
diff --git a/coder-apps/install_app.cmd b/coder-apps/install_app.cmd
new file mode 100644
index 00000000..fbf30b20
--- /dev/null
+++ b/coder-apps/install_app.cmd
@@ -0,0 +1,20 @@
+@echo off
+if =%3-==-- echo "Usage: install_app.cmd [appname] [coderbase] [apppath]" & exit /b
+
+set app=%1
+set base=%2
+set from=%3
+
+
+mkdir %base%\apps\%app%
+mkdir %base%\static\apps\%app%
+mkdir %base%\static\apps\%app%\js
+mkdir %base%\static\apps\%app%\css
+mkdir %base%\static\apps\%app%\media
+mkdir %base%\views\apps\%app%
+
+copy %from%\%app%\app\*.* %base%\apps\%app%
+copy %from%\%app%\views\*.* %base%\views\apps\%app%
+copy %from%\%app%\static\js\*.* %base%\static\apps\%app%\js
+copy %from%\%app%\static\css\*.* %base%\static\apps\%app%\css
+copy %from%\%app%\static\media\*.* %base%\static\apps\%app%\media
\ No newline at end of file
diff --git a/coder-apps/install_app.sh b/coder-apps/install_app.sh
index 774a328a..23b92983 100755
--- a/coder-apps/install_app.sh
+++ b/coder-apps/install_app.sh
@@ -19,12 +19,12 @@ app=$1
base=$2
from=$3
-mkdir "$base/apps/$app"
-mkdir "$base/static/apps/$app"
-mkdir "$base/static/apps/$app/js"
-mkdir "$base/static/apps/$app/css"
-mkdir "$base/static/apps/$app/media"
-mkdir "$base/views/apps/$app"
+mkdir -p "$base/apps/$app"
+mkdir -p "$base/static/apps/$app"
+mkdir -p "$base/static/apps/$app/js"
+mkdir -p "$base/static/apps/$app/css"
+mkdir -p "$base/static/apps/$app/media"
+mkdir -p "$base/views/apps/$app"
cp $from/$app/app/* $base/apps/$app/
cp $from/$app/views/* $base/views/apps/$app/
diff --git a/coder-apps/install_common.cmd b/coder-apps/install_common.cmd
new file mode 100644
index 00000000..bc8fc989
--- /dev/null
+++ b/coder-apps/install_common.cmd
@@ -0,0 +1,57 @@
+@echo off
+if =%1-==-- echo "Usage: install_common.cmd [coderbase]" & exit /b
+
+echo "Creating folders...."
+
+set base=%1
+
+REM Create base folders
+IF NOT EXIST %base% (
+ mkdir %base%
+)
+
+if NOT EXIST %base%\apps (
+ mkdir %base%\apps
+)
+
+if NOT EXIST %base%\static (
+ mkdir %base%\static
+)
+
+if NOT EXIST %base%\static\apps (
+ mkdir %base%\static\apps
+)
+
+if NOT EXIST %base%\views (
+ mkdir %base%\views
+)
+
+if NOT EXIST %base%\views\apps (
+ mkdir %base%\views\apps
+)
+
+echo "Copying apps"
+
+call install_app.cmd auth %base% .\common\
+call install_app.cmd boilerplate %base% .\common\
+call install_app.cmd coder %base% .\common\
+call install_app.cmd coderlib %base% .\common\
+call install_app.cmd editor %base% .\common\
+call install_app.cmd eyeball %base% .\common\
+call install_app.cmd game2d %base% .\common\
+call install_app.cmd hello_coder %base% .\common\
+call install_app.cmd space_rocks_ %base% .\common\
+call install_app.cmd localauth %base% .\common\
+
+REM RVA Coder Dojo extra bundles
+call install_app.cmd pop_up_penguins %base% .\common\
+call install_app.cmd comic_creator %base% .\common\
+call install_app.cmd rva_coderdojo_matrix %base% .\common\
+
+REM Assets for other projects used in RVA Coder Dojo
+if NOT EXIST %base%\project-assets (
+ mkdir %base%\project-assets
+)
+xcopy ..\project-assets\*.* %base%\project-assets /E
+
+echo "Copy completed"
diff --git a/coder-apps/install_common.sh b/coder-apps/install_common.sh
index a2ba85e8..3d860524 100755
--- a/coder-apps/install_common.sh
+++ b/coder-apps/install_common.sh
@@ -26,4 +26,14 @@ base=$1
./install_app.sh game2d $base ./common/
./install_app.sh hello_coder $base ./common/
./install_app.sh space_rocks_ $base ./common/
+./install_app.sh localauth $base ./common/
+
+## RVA Coder Dojo extra bundles
+./install_app.sh pop_up_penguins $base ./common/
+./install_app.sh comic_creator $base ./common/
+./install_app.sh rva_coderdojo_matrix $base ./common/
+
+## Assets for other projects used in RVA Coder Dojo
+mkdir -p $base/project-assets
+cp -r ../project-assets/* $base/project-assets
diff --git a/coder-base/config.js b/coder-base/config.js
index 19052a53..f4c5f53b 100644
--- a/coder-base/config.js
+++ b/coder-base/config.js
@@ -1,7 +1,7 @@
exports.listenIP = null; //Defaults to *
-exports.listenPort = '8081'; //the SSL port things run on
-exports.httpListenPort = '8080'; //this will all be redirected to SSL
+exports.listenPort = '9081'; //the SSL port things run on
+exports.httpListenPort = '9080'; //this will all be redirected to SSL
exports.cacheApps = true;
exports.httpVisiblePort = '80'; //forwarded http port the user sees
exports.httpsVisiblePort = '443'; //forwarded https port the user sees
diff --git a/coder-base/localserver.js b/coder-base/localserver.js
index 1ef8034f..f06c1122 100644
--- a/coder-base/localserver.js
+++ b/coder-base/localserver.js
@@ -20,7 +20,6 @@
var express = require('express');
-var io = require('socket.io');
var net = require('http');
var http = require('http');
var https = require('https');
@@ -64,15 +63,18 @@ var apphandler = function( req, res, appdir ) {
util.log( "GET: " + apppath + " " + appname );
+ auth = require(appdir + "localauth" + "/app");
+
+ /* Disabling authentication requirement. */
//Redirect to sign-in for unauthenticated users
- publicAllowed = ["auth"]; //apps that are exempt from any login (should only be auth)
- auth = require(appdir + "auth" + "/app");
- user = auth.isAuthenticated(req, res);
- if ( !user && publicAllowed.indexOf( appname ) < 0) {
- util.log("redirect: " + "http://" + getHost(req) + ":" + config.httpVisiblePort + '/app/auth');
- res.redirect("http://" + getHost(req) + ":" + config.httpVisiblePort + '/app/auth' );
- return;
- }
+// publicAllowed = ["auth"]; //apps that are exempt from any login (should only be auth)
+// auth = require(appdir + "auth" + "/app");
+// user = auth.isAuthenticated(req, res);
+// if ( !user && publicAllowed.indexOf( appname ) < 0) {
+// util.log("redirect: " + "http://" + getHost(req) + ":" + config.httpVisiblePort + '/app/auth');
+// res.redirect("http://" + getHost(req) + ":" + config.httpVisiblePort + '/app/auth' );
+// return;
+// }
if ( !apppath ) {
@@ -133,7 +135,8 @@ var apphandler = function( req, res, appdir ) {
var startLocal = function() {
- http.createServer(localapp).listen( config.httpListenPort, '127.0.0.1' );
+ http.createServer(localapp).listen( config.httpListenPort, 'localhost' );
+ console.log('Listening on: http://127.0.0.1:' + config.httpListenPort);
};
var getHost = function( req ) {
@@ -162,7 +165,7 @@ localapp.use( express.session({
localapp.use( '/static', express.static( __dirname + '/static' ) );
localapp.get( '/', function( req, res ) {
util.log( 'GET: /' );
- res.redirect( '/app/auth' );
+ res.redirect( '/app/localauth' );
});
localapp.all( /^\/app\/(\w+)\/(.*)$/, function( req, res ) { apphandler( req, res, __dirname + '/apps/'); } );
localapp.all( /^\/app\/(\w+)\/$/, function( req, res ) { apphandler( req, res, __dirname + '/apps/'); } );
diff --git a/coder-base/package.json b/coder-base/package.json
index e42be37a..fbc4b1d9 100644
--- a/coder-base/package.json
+++ b/coder-base/package.json
@@ -8,9 +8,8 @@
"redis": "0.8.2",
"mustache": "0.7.2",
"consolidate": "0.8.0",
- "socket.io": "0.9.13",
"express-params": "0.0.3",
- "bcrypt": "0.7.4",
+ "bcrypt-nodejs": "*",
"connect": "*",
"cookie": "*"
}
diff --git a/coderdojo/nodejs/node-mac.tar.gz b/coderdojo/nodejs/node-mac.tar.gz
new file mode 100644
index 00000000..0cff16dc
Binary files /dev/null and b/coderdojo/nodejs/node-mac.tar.gz differ
diff --git a/coderdojo/nodejs/node.exe b/coderdojo/nodejs/node.exe
new file mode 100644
index 00000000..324fb8ce
Binary files /dev/null and b/coderdojo/nodejs/node.exe differ
diff --git a/coderdojo/scripts/run-coder.cmd b/coderdojo/scripts/run-coder.cmd
new file mode 100644
index 00000000..a2af2d9a
--- /dev/null
+++ b/coderdojo/scripts/run-coder.cmd
@@ -0,0 +1,3 @@
+@echo off
+echo "Open your browser to the address shown below."
+node\node localserver.js
\ No newline at end of file
diff --git a/coderdojo/scripts/run-coder.sh b/coderdojo/scripts/run-coder.sh
new file mode 100644
index 00000000..c0c0bb3b
--- /dev/null
+++ b/coderdojo/scripts/run-coder.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+echo "Open your browser to the address shown below."
+node/bin/node localserver.js
diff --git a/project-assets/comic_creator/baby.jpg b/project-assets/comic_creator/baby.jpg
new file mode 100644
index 00000000..ba0b9648
Binary files /dev/null and b/project-assets/comic_creator/baby.jpg differ
diff --git a/project-assets/comic_creator/landscape.jpg b/project-assets/comic_creator/landscape.jpg
new file mode 100644
index 00000000..10aa0b92
Binary files /dev/null and b/project-assets/comic_creator/landscape.jpg differ
diff --git a/project-assets/comic_creator/penguin.jpg b/project-assets/comic_creator/penguin.jpg
new file mode 100644
index 00000000..e31998eb
Binary files /dev/null and b/project-assets/comic_creator/penguin.jpg differ
diff --git a/project-assets/comic_creator/running.jpg b/project-assets/comic_creator/running.jpg
new file mode 100644
index 00000000..f333ff44
Binary files /dev/null and b/project-assets/comic_creator/running.jpg differ
diff --git a/project-assets/comic_creator/sleds.jpg b/project-assets/comic_creator/sleds.jpg
new file mode 100644
index 00000000..11d490ab
Binary files /dev/null and b/project-assets/comic_creator/sleds.jpg differ
diff --git a/project-assets/the_perfect_sandwich/the_perfect_pbj.jpg b/project-assets/the_perfect_sandwich/the_perfect_pbj.jpg
new file mode 100644
index 00000000..3404d49b
Binary files /dev/null and b/project-assets/the_perfect_sandwich/the_perfect_pbj.jpg differ