├── client ├── vendor │ └── .gitkeep ├── app │ ├── helpers │ │ ├── .gitkeep │ │ ├── repeat-text.js │ │ └── output-value.js │ ├── models │ │ ├── .gitkeep │ │ ├── tag.js │ │ ├── project.js │ │ └── post.js │ ├── routes │ │ ├── .gitkeep │ │ ├── 404.js │ │ ├── auth.js │ │ ├── stream.js │ │ ├── auth │ │ │ └── login.js │ │ ├── about.js │ │ ├── blog-posts │ │ │ ├── tag-filtered.js │ │ │ ├── posts-list.js │ │ │ ├── posts-list-page.js │ │ │ ├── post-edit.js │ │ │ ├── post-new.js │ │ │ └── post-read.js │ │ ├── projects.js │ │ ├── application.js │ │ ├── blog-posts.js │ │ └── projects │ │ │ ├── list.js │ │ │ └── create.js │ ├── components │ │ ├── .gitkeep │ │ ├── tags-list.js │ │ ├── admin-toolbar.js │ │ ├── lctv-frame.js │ │ ├── blog-post-body.js │ │ ├── nav-bar.js │ │ ├── image-uploader.js │ │ ├── project-edit-form.js │ │ ├── autocomplete-input.js │ │ └── post-edit-form.js │ ├── controllers │ │ ├── .gitkeep │ │ ├── projects.js │ │ ├── application.js │ │ ├── stream.js │ │ ├── auth │ │ │ └── login.js │ │ └── blog-posts.js │ ├── styles │ │ ├── _projects.scss │ │ ├── blog-posts │ │ │ ├── _post-new.scss │ │ │ ├── _post-read.scss │ │ │ └── _posts-list.scss │ │ ├── _helpers.scss │ │ ├── components │ │ │ ├── _lctv-frame.scss │ │ │ ├── _tags-list.scss │ │ │ ├── _image-uploader.scss │ │ │ ├── _autocomplete-input.scss │ │ │ ├── _admin-toolbar.scss │ │ │ ├── _post-edit-form.scss │ │ │ ├── _blog-post-body.scss │ │ │ └── _nav-bar.scss │ │ ├── _mixins.scss │ │ ├── _about.scss │ │ ├── auth │ │ │ └── _login.scss │ │ ├── _bits.scss │ │ ├── _a2a.scss │ │ ├── app.scss │ │ ├── _application.scss │ │ ├── _generic.scss │ │ └── _media-queries.scss │ ├── templates │ │ ├── auth.hbs │ │ ├── projects.hbs │ │ ├── stream.hbs │ │ ├── blog-posts.hbs │ │ ├── components │ │ │ ├── blog-post-body.hbs │ │ │ ├── admin-toolbar.hbs │ │ │ ├── tags-list.hbs │ │ │ ├── lctv-frame.hbs │ │ │ ├── autocomplete-input.hbs │ │ │ ├── image-uploader.hbs │ │ │ ├── nav-bar.hbs │ │ │ ├── project-edit-form.hbs │ │ │ └── post-edit-form.hbs │ │ ├── blog-posts │ │ │ ├── post-new.hbs │ │ │ ├── post-edit.hbs │ │ │ ├── tag-filtered.hbs │ │ │ ├── posts-list.hbs │ │ │ └── post-read.hbs │ │ ├── projects │ │ │ ├── create.hbs │ │ │ └── list.hbs │ │ ├── head.hbs │ │ ├── 404.hbs │ │ ├── application.hbs │ │ ├── auth │ │ │ └── login.hbs │ │ └── about.hbs │ ├── resolver.js │ ├── authorizers │ │ └── oauth2.js │ ├── serializers │ │ ├── application.js │ │ ├── tag.js │ │ └── post.js │ ├── authenticators │ │ └── oauth2.js │ ├── storages │ │ └── post-backup.js │ ├── adapters │ │ └── application.js │ ├── app.js │ ├── initializers │ │ ├── scroll-reset.js │ │ ├── reset-navbar.js │ │ └── nav-indicator.js │ ├── services │ │ ├── lctv.js │ │ ├── user.js │ │ └── api.js │ ├── index.html │ └── router.js ├── tests │ ├── unit │ │ ├── .gitkeep │ │ ├── helpers │ │ │ ├── repeat-test.js │ │ │ ├── repeat-text-test.js │ │ │ └── output-value-test.js │ │ ├── routes │ │ │ ├── 404-test.js │ │ │ ├── about-test.js │ │ │ ├── auth-test.js │ │ │ ├── stream-test.js │ │ │ ├── projects-test.js │ │ │ ├── auth │ │ │ │ └── login-test.js │ │ │ ├── blog-posts-test.js │ │ │ ├── projects │ │ │ │ ├── list-test.js │ │ │ │ └── create-test.js │ │ │ └── blog-posts │ │ │ │ ├── post-edit-test.js │ │ │ │ ├── post-new-test.js │ │ │ │ ├── post-read-test.js │ │ │ │ ├── posts-list-test.js │ │ │ │ └── tag-filtered-test.js │ │ ├── models │ │ │ ├── tag-test.js │ │ │ └── post-test.js │ │ ├── services │ │ │ ├── api-test.js │ │ │ └── user-test.js │ │ ├── adapters │ │ │ └── application-test.js │ │ ├── controllers │ │ │ ├── projects-test.js │ │ │ ├── application-test.js │ │ │ ├── auth │ │ │ │ └── login-test.js │ │ │ └── blog-posts-test.js │ │ └── initializers │ │ │ ├── nav-indicator-test.js │ │ │ └── webfont-loader-test.js │ ├── integration │ │ ├── .gitkeep │ │ └── components │ │ │ ├── blog-post-body-test.js │ │ │ ├── lctv-frame-test.js │ │ │ ├── tags-list-test.js │ │ │ ├── image-uploader-test.js │ │ │ ├── project-edit-form-test.js │ │ │ ├── autocomplete-input-test.js │ │ │ ├── admin-toolbar-test.js │ │ │ ├── nav-bar-test.js │ │ │ └── post-edit-form-test.js │ ├── test-helper.js │ ├── helpers │ │ ├── destroy-app.js │ │ ├── resolver.js │ │ ├── start-app.js │ │ └── module-for-acceptance.js │ ├── .jshintrc │ └── index.html ├── server │ ├── .jshintrc │ ├── index.js │ └── mocks │ │ └── posts.js ├── .watchmanconfig ├── public │ ├── robots.txt │ └── crossdomain.xml ├── .bowerrc ├── testem.json ├── testem.js ├── .ember-cli ├── .gitignore ├── .editorconfig ├── .travis.yml ├── bower.json ├── .jshintrc ├── config │ ├── deploy.js │ └── environment.js ├── ember-cli-build.js ├── README.md └── package.json ├── cache ├── .jshintrc ├── package.json ├── phantom.js ├── bin │ └── www └── app.js ├── server ├── .jshintrc ├── api │ ├── feed │ │ ├── index.js │ │ └── route.js │ ├── users │ │ ├── index.js │ │ └── route.js │ ├── tags │ │ ├── index.js │ │ └── route.js │ ├── index.js │ ├── projects │ │ ├── index.js │ │ └── route.js │ ├── auth │ │ └── index.js │ └── posts │ │ ├── index.js │ │ └── route.js ├── config.template.json ├── models │ ├── tag.js │ ├── project.js │ ├── post.js │ └── user.js ├── auth │ ├── index.js │ ├── local │ │ ├── index.js │ │ └── passport.js │ └── service.js ├── package.json ├── views │ └── rss.jade ├── bin │ └── www └── app.js ├── README.md ├── .gitignore ├── .jshintrc └── .editorconfig /client/vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/app/styles/_projects.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/tests/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/app/styles/blog-posts/_post-new.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/app/templates/auth.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /cache/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true 3 | } 4 | 5 | -------------------------------------------------------------------------------- /client/server/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true 3 | } 4 | -------------------------------------------------------------------------------- /server/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | esnext: true 4 | } 5 | -------------------------------------------------------------------------------- /client/.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /client/app/templates/projects.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | {{title "Projects"}} 3 | -------------------------------------------------------------------------------- /client/app/templates/stream.hbs: -------------------------------------------------------------------------------- 1 | {{lctv-frame}} 2 | {{outlet}} 3 | 4 | -------------------------------------------------------------------------------- /client/app/templates/blog-posts.hbs: -------------------------------------------------------------------------------- 1 | {{title "Blog"}} 2 | 3 | {{outlet}} 4 | -------------------------------------------------------------------------------- /client/app/styles/_helpers.scss: -------------------------------------------------------------------------------- 1 | .hidden { 2 | display: none !important; 3 | } 4 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /client/app/styles/components/_lctv-frame.scss: -------------------------------------------------------------------------------- 1 | #lctv-frame { 2 | margin-bottom: 40px; 3 | } 4 | -------------------------------------------------------------------------------- /client/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /client/app/routes/404.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /client/app/templates/components/blog-post-body.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{yield}} 3 |
4 | -------------------------------------------------------------------------------- /client/app/routes/auth.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /client/app/routes/stream.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /client/app/routes/auth/login.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /client/app/components/tags-list.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /client/app/templates/blog-posts/post-new.hbs: -------------------------------------------------------------------------------- 1 | {{post-edit-form save="savePost" form-title="New post" post=model}} 2 | {{outlet}} 3 | -------------------------------------------------------------------------------- /client/app/templates/projects/create.hbs: -------------------------------------------------------------------------------- 1 | {{project-edit-form save="save" form-title="New project" project=model}} 2 | {{outlet}} 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-express-blog 2 | 3 | Just my another atempt to learn Ember. 4 | 5 | Will become a main blogging system for me. 6 | -------------------------------------------------------------------------------- /client/app/templates/blog-posts/post-edit.hbs: -------------------------------------------------------------------------------- 1 | {{post-edit-form save="savePost" form-title="Edit post" post=model }} 2 | {{outlet}} 3 | -------------------------------------------------------------------------------- /client/app/models/tag.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | name: DS.attr('string') 5 | }); 6 | 7 | -------------------------------------------------------------------------------- /client/app/authorizers/oauth2.js: -------------------------------------------------------------------------------- 1 | import OAuth2Bearer from 'ember-simple-auth/authorizers/oauth2-bearer'; 2 | 3 | export default OAuth2Bearer.extend(); 4 | -------------------------------------------------------------------------------- /client/app/serializers/application.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.RESTSerializer.extend({ 4 | primaryKey: '_id' 5 | }); 6 | -------------------------------------------------------------------------------- /client/app/components/admin-toolbar.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | user: Ember.inject.service() 5 | }); 6 | -------------------------------------------------------------------------------- /client/app/components/lctv-frame.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | lctv: Ember.inject.service(), 5 | }); 6 | -------------------------------------------------------------------------------- /client/tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | 6 | setResolver(resolver); 7 | -------------------------------------------------------------------------------- /client/tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default function destroyApp(application) { 4 | Ember.run(application, 'destroy'); 5 | } 6 | -------------------------------------------------------------------------------- /client/app/serializers/tag.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, { 4 | primaryKey: '_id' 5 | }); 6 | 7 | -------------------------------------------------------------------------------- /client/app/templates/components/admin-toolbar.hbs: -------------------------------------------------------------------------------- 1 | {{#if user.isAdmin}} 2 |
3 | 6 |
7 | {{/if}} 8 | -------------------------------------------------------------------------------- /client/app/templates/components/tags-list.hbs: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /client/app/controllers/projects.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | session: Ember.inject.service(), 5 | user: Ember.inject.service() 6 | }); 7 | -------------------------------------------------------------------------------- /server/api/feed/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var feed = require('./route'); 4 | 5 | router.get('/rss', feed.rss); 6 | 7 | module.exports = router; 8 | -------------------------------------------------------------------------------- /client/app/routes/about.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ENV from '../config/environment'; 3 | 4 | export default Ember.Route.extend({ 5 | model() { 6 | return ENV['author'] || {}; 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /client/app/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin tag-style() { 2 | font-size: 90%; 3 | font-weight: 500; 4 | color: $grey; 5 | background-color: $grey-lightest; 6 | padding: 2px 5px; 7 | cursor: default; 8 | } 9 | -------------------------------------------------------------------------------- /client/app/routes/blog-posts/tag-filtered.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model(tag) { 5 | let name = tag.tag; 6 | return this.store.query('post', { tag: name }); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /client/app/templates/head.hbs: -------------------------------------------------------------------------------- 1 | {{!-- 2 | Add content you wish automatically added to the documents head 3 | here. The 'model' available in this template can be populated by 4 | setting values on the 'head-data' service. 5 | --}} 6 | {{model.title}} 7 | -------------------------------------------------------------------------------- /client/testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "qunit", 3 | "test_page": "tests/index.html?hidepassed", 4 | "disable_watching": true, 5 | "launch_in_ci": [ 6 | "PhantomJS" 7 | ], 8 | "launch_in_dev": [ 9 | "PhantomJS", 10 | "Chrome" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /server/config.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "secret": "", 3 | "database": "mongodb://localhost/blog", 4 | "googleAuth": { 5 | "clientID": "", 6 | "clientSecret": "", 7 | "callbackURL": "http://localhost:3000/api/auth/callback" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client/app/styles/_about.scss: -------------------------------------------------------------------------------- 1 | .about { 2 | p { 3 | padding-bottom: 10px; 4 | } 5 | img { 6 | margin: 0; 7 | margin-bottom: 20px; 8 | } 9 | ul { 10 | li:before { 11 | content: '* '; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/app/templates/404.hbs: -------------------------------------------------------------------------------- 1 |

404

2 |

Sorry

3 |
Page not found.
4 |

No matter how long you are going to wait here, the page you requested will not come back.

5 | Akito dog 6 | -------------------------------------------------------------------------------- /client/app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | session: Ember.inject.service('session'), 5 | user: Ember.inject.service('user'), 6 | 7 | init() { 8 | this._super(...arguments); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /client/app/styles/components/_tags-list.scss: -------------------------------------------------------------------------------- 1 | .tags-list { 2 | list-style: none; 3 | margin: 0; 4 | padding: 0; 5 | 6 | li { 7 | display: inline-block; 8 | span { 9 | @include tag-style; 10 | } 11 | padding: 10px 0; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/models/tag.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | 4 | var TagSchema = new Schema({ 5 | name: String, 6 | // posts: [{ type: Schema.Types.ObjectId, ref: 'Post' }], 7 | }); 8 | 9 | module.exports = mongoose.model('Tag', TagSchema); 10 | 11 | -------------------------------------------------------------------------------- /client/app/styles/auth/_login.scss: -------------------------------------------------------------------------------- 1 | .login-form { 2 | .error-message { 3 | padding: 12px 20px; 4 | color: white; 5 | background-color: $red; 6 | 7 | .message-title { 8 | margin-top: 0; 9 | font-weight: 700; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /client/app/routes/projects.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | actions: { 5 | save(project) { 6 | project.save().then(() => { 7 | this.transitionTo('projects.list'); 8 | }); 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /client/app/serializers/post.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, { 4 | primaryKey: '_id', 5 | attrs: { 6 | 'tags': { 7 | serialize: 'ids', 8 | deserialize: 'records' 9 | } 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /client/app/styles/blog-posts/_post-read.scss: -------------------------------------------------------------------------------- 1 | .post { 2 | p { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .hint { 8 | cursor: help; 9 | border-bottom: 1px dashed $grey; 10 | } 11 | 12 | .comments { 13 | margin-top: 20px; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /client/testem.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | "framework": "qunit", 4 | "test_page": "tests/index.html?hidepassed", 5 | "disable_watching": true, 6 | "launch_in_ci": [ 7 | "PhantomJS" 8 | ], 9 | "launch_in_dev": [ 10 | "PhantomJS", 11 | "Chrome" 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /client/app/authenticators/oauth2.js: -------------------------------------------------------------------------------- 1 | import OAuth2PasswordGrant from 'ember-simple-auth/authenticators/oauth2-password-grant'; 2 | import config from '../config/environment'; 3 | 4 | export default OAuth2PasswordGrant.extend({ 5 | serverTokenEndpoint: [config.API.host, config.API.namespace, 'auth/local'].join('/') 6 | }); 7 | -------------------------------------------------------------------------------- /client/app/templates/components/lctv-frame.hbs: -------------------------------------------------------------------------------- 1 | {{#if isLive}} 2 |

I'm streaming

3 | 4 | {{else}} 5 |

I'm not streaming

6 | {{/if}} 7 | 8 | {{yield}} 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist 5 | tmp 6 | 7 | # dependencies 8 | node_modules 9 | bower_components 10 | 11 | # misc 12 | .sass-cache 13 | connect.lock 14 | coverage/* 15 | libpeerconnection.log 16 | npm-debug.log 17 | testem.log 18 | -------------------------------------------------------------------------------- /client/.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": true 9 | } 10 | -------------------------------------------------------------------------------- /client/tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /client/tests/unit/helpers/repeat-test.js: -------------------------------------------------------------------------------- 1 | import { repeat } from '../../../helpers/repeat'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | repeat'); 5 | 6 | // Replace this with your real tests. 7 | test('it works', function(assert) { 8 | let result = repeat(42); 9 | assert.ok(result); 10 | }); 11 | -------------------------------------------------------------------------------- /client/app/storages/post-backup.js: -------------------------------------------------------------------------------- 1 | import StorageObject from 'ember-local-storage/local/object'; 2 | 3 | const Storage = StorageObject.extend(); 4 | 5 | // Uncomment if you would like to set initialState 6 | // Storage.reopenClass({ 7 | // initialState() { 8 | // return {}; 9 | // } 10 | // }); 11 | 12 | export default Storage; -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log* 17 | testem.log 18 | -------------------------------------------------------------------------------- /client/app/components/blog-post-body.js: -------------------------------------------------------------------------------- 1 | /* globals hljs */ 2 | import Ember from 'ember'; 3 | 4 | export default Ember.Component.extend({ 5 | didInsertElement() { 6 | this._super(...arguments); 7 | 8 | Ember.$('pre code').each((i, block) => { 9 | hljs.highlightBlock(block); 10 | }); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /client/app/helpers/repeat-text.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export function repeatText(params/*, hash*/) { 4 | let times = parseInt(params[0], 10), 5 | text = params[1]; 6 | 7 | let result = (new Array(times + 1)).join(text); 8 | 9 | return result; 10 | } 11 | 12 | export default Ember.Helper.helper(repeatText); 13 | -------------------------------------------------------------------------------- /client/tests/unit/helpers/repeat-text-test.js: -------------------------------------------------------------------------------- 1 | import { repeatText } from '../../../helpers/repeat-text'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | repeat text'); 5 | 6 | // Replace this with your real tests. 7 | test('it works', function(assert) { 8 | let result = repeatText(42); 9 | assert.ok(result); 10 | }); 11 | -------------------------------------------------------------------------------- /client/tests/unit/routes/404-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:404', 'Unit | Route | 404', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /client/tests/unit/routes/about-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:about', 'Unit | Route | about', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /client/tests/unit/routes/auth-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:auth', 'Unit | Route | auth', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /client/tests/unit/routes/stream-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:stream', 'Unit | Route | stream', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /client/app/adapters/application.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import config from '../config/environment'; 3 | import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin'; 4 | 5 | export default DS.RESTAdapter.extend(DataAdapterMixin, { 6 | authorizer: 'authorizer:oauth2', 7 | host: config.API.host, 8 | namespace: config.API.namespace 9 | }); 10 | -------------------------------------------------------------------------------- /client/tests/unit/routes/projects-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:projects', 'Unit | Route | projects', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cache", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "debug": "^2.2.0", 13 | "express": "^4.13.4", 14 | "redis": "^2.4.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/tests/unit/routes/auth/login-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:auth/login', 'Unit | Route | auth/login', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /client/tests/unit/routes/blog-posts-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:blog-posts', 'Unit | Route | blog posts', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /client/tests/unit/routes/projects/list-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:projects/list', 'Unit | Route | projects/list', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /client/app/styles/_bits.scss: -------------------------------------------------------------------------------- 1 | $accent: #FF8300; 2 | $green: rgb(84, 179, 108); 3 | $red: rgb(179, 84, 84); 4 | 5 | $grey-lightest: rgba(0, 0, 0, 0.05); 6 | $grey-light: rgba(0, 0, 0, 0.3); 7 | $grey: rgba(0, 0, 0, 0.6); 8 | $grey-dark: rgba(0, 0, 0, 0.8); 9 | $grey-darkest: rgba(0, 0, 0, 0.9); 10 | 11 | $font-size-title: 32px; 12 | $font-size-navbar: 26px; 13 | 14 | $text-color: $grey-dark; 15 | -------------------------------------------------------------------------------- /client/tests/unit/routes/projects/create-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:projects/create', 'Unit | Route | projects/create', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /client/tests/unit/models/tag-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForModel, test } from 'ember-qunit'; 2 | 3 | moduleForModel('tag', 'Unit | Model | tag', { 4 | // Specify the other units that are required for this test. 5 | needs: [] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let model = this.subject(); 10 | // let store = this.store(); 11 | assert.ok(!!model); 12 | }); 13 | 14 | -------------------------------------------------------------------------------- /client/tests/unit/models/post-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForModel, test } from 'ember-qunit'; 2 | 3 | moduleForModel('post', 'Unit | Model | post', { 4 | // Specify the other units that are required for this test. 5 | needs: [ 'model:tag' ] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let model = this.subject(); 10 | // let store = this.store(); 11 | assert.ok(!!model); 12 | }); 13 | -------------------------------------------------------------------------------- /client/tests/unit/routes/blog-posts/post-edit-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:blog-posts/post-edit', 'Unit | Route | blog posts/post edit', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /client/tests/unit/routes/blog-posts/post-new-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:blog-posts/post-new', 'Unit | Route | blog posts/post new', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /client/tests/unit/routes/blog-posts/post-read-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:blog-posts/post-read', 'Unit | Route | blog posts/post read', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /client/tests/unit/services/api-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('service:api', 'Unit | Service | api', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['service:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | let service = this.subject(); 11 | assert.ok(service); 12 | }); 13 | -------------------------------------------------------------------------------- /client/app/controllers/stream.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | lctv: Ember.inject.service(), 5 | 6 | init() { 7 | this.get('lctv').checkStatus(); 8 | this._super(...arguments); 9 | }, 10 | 11 | isLiveChanged: Ember.observer('lctv.isLive', function() { 12 | this.set('isLive', this.get('lctv.isLive')); 13 | }) 14 | }); 15 | -------------------------------------------------------------------------------- /client/tests/unit/routes/blog-posts/posts-list-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:blog-posts/posts-list', 'Unit | Route | blog posts/posts list', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /client/tests/unit/routes/blog-posts/tag-filtered-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('route:blog-posts/tag-filtered', 'Unit | Route | blog posts/tag filtered', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | test('it exists', function(assert) { 9 | let route = this.subject(); 10 | assert.ok(route); 11 | }); 12 | -------------------------------------------------------------------------------- /server/auth/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var passport = require('passport'); 5 | var config = require('../config/config'); 6 | var User = require('../models/user'); 7 | 8 | // Passport Configuration 9 | require('./local/passport').setup(User, config); 10 | 11 | var router = express.Router(); 12 | 13 | router.use('/local', require('./local')); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /server/api/users/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var users = require('./route'); 4 | var auth = require('../../auth/service'); 5 | 6 | router.post('/', auth.hasRole('admin'), users.create); 7 | router.get('/', auth.hasRole('admin'), users.index); 8 | router.get('/me', auth.isAuthenticated(), users.me); 9 | router.get('/setup', users.setup); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /client/tests/unit/adapters/application-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('adapter:application', 'Unit | Adapter | application', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['serializer:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | let adapter = this.subject(); 11 | assert.ok(adapter); 12 | }); 13 | -------------------------------------------------------------------------------- /client/app/models/project.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | name: DS.attr('string'), 5 | url: DS.attr('string'), 6 | description: DS.attr('string'), 7 | githubID: DS.attr('number'), 8 | dateCreated: DS.attr('date'), 9 | dateUpdated: DS.attr('date'), 10 | stars: DS.attr('number'), 11 | isOwner: DS.attr('boolean'), 12 | isPublished: DS.attr('boolean') 13 | }); 14 | -------------------------------------------------------------------------------- /client/tests/unit/controllers/projects-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('controller:projects', 'Unit | Controller | projects', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | let controller = this.subject(); 11 | assert.ok(controller); 12 | }); 13 | -------------------------------------------------------------------------------- /client/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{title model.fullname prepend=true}} 2 |
3 | {{nav-bar title=model.fullname}} 4 |
5 | {{outlet}} 6 | 7 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /client/tests/unit/controllers/application-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('controller:application', 'Unit | Controller | application', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | let controller = this.subject(); 11 | assert.ok(controller); 12 | }); 13 | -------------------------------------------------------------------------------- /client/tests/unit/controllers/auth/login-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('controller:auth/login', 'Unit | Controller | auth/login', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | let controller = this.subject(); 11 | assert.ok(controller); 12 | }); 13 | -------------------------------------------------------------------------------- /client/tests/unit/controllers/blog-posts-test.js: -------------------------------------------------------------------------------- 1 | import { moduleFor, test } from 'ember-qunit'; 2 | 3 | moduleFor('controller:blog-posts', 'Unit | Controller | blog posts', { 4 | // Specify the other units that are required for this test. 5 | // needs: ['controller:foo'] 6 | }); 7 | 8 | // Replace this with your real tests. 9 | test('it exists', function(assert) { 10 | let controller = this.subject(); 11 | assert.ok(controller); 12 | }); 13 | -------------------------------------------------------------------------------- /client/app/routes/blog-posts/posts-list.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | controllerName: 'blog-posts', 5 | 6 | model() { 7 | let ctrl = this.controllerFor('blog-posts'); 8 | let query = { 9 | page: 1, 10 | size: ctrl.get('pageSize') 11 | }; 12 | ctrl.set('currentPage', 1); 13 | return this.store.query('post', query); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /client/app/styles/components/_image-uploader.scss: -------------------------------------------------------------------------------- 1 | .dropzone { 2 | margin-bottom: 20px; 3 | #upload-image { 4 | p { 5 | border: 1px solid; 6 | padding: 20px; 7 | text-align: center; 8 | } 9 | } 10 | } 11 | 12 | .uploaded-files { 13 | li { 14 | img { 15 | display: inline-block; 16 | margin: 0; 17 | max-width: 200px; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server/api/tags/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var auth = require('../../auth/service'); 4 | var tags = require('./route'); 5 | 6 | router.post('/', auth.hasRole('admin'), tags.add); 7 | router.get('/', tags.getAll); 8 | router.put('/:id', auth.hasRole('admin'), tags.update); 9 | router.get('/:id', tags.getOne); 10 | router.delete('/:id', auth.hasRole('admin'), tags.delete); 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /client/app/models/post.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | title: DS.attr('string'), 5 | body: DS.attr('string'), 6 | markdown: DS.attr('string'), 7 | slug: DS.attr('string'), 8 | description: DS.attr('string'), 9 | dateCreated: DS.attr('date'), 10 | isPublished: DS.attr('boolean'), 11 | tags: DS.hasMany('tag', { async: true }), 12 | project: DS.belongsTo('project', { async: true }) 13 | }); 14 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /client/app/styles/_a2a.scss: -------------------------------------------------------------------------------- 1 | .a2a_default_style .a2a_count, 2 | .a2a_default_style .a2a_svg, 3 | .a2a_floating_style .a2a_svg, 4 | .a2a_vertical_style .a2a_count, 5 | .a2a_menu .a2a_svg { 6 | border-radius: 0 !important; 7 | font-family: 'Roboto Mono', monospace; 8 | line-height: 19px !important; 9 | } 10 | 11 | .a2a_kit { 12 | width: 288px; 13 | margin: 0 auto; 14 | float: right; 15 | 16 | &.a2a_kit-bottom { 17 | margin-top: 25px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | let App; 7 | 8 | Ember.MODEL_FACTORY_INJECTIONS = true; 9 | 10 | App = Ember.Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver 14 | }); 15 | 16 | loadInitializers(App, config.modulePrefix); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /client/app/routes/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from '../config/environment'; 3 | 4 | import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin'; 5 | 6 | export default Ember.Route.extend(ApplicationRouteMixin, { 7 | model() { 8 | const author = config['author'] || {}; 9 | const fullname = [ author.name, author.lastName ].join(' '); 10 | author.fullname = fullname; 11 | return author; 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /client/app/styles/blog-posts/_posts-list.scss: -------------------------------------------------------------------------------- 1 | .list { 2 | .pagination { 3 | div { 4 | height: 21px; 5 | } 6 | &.pagination-bottom { 7 | margin-top: -20px; 8 | margin-bottom: 20px; 9 | } 10 | .older { 11 | float: right; 12 | } 13 | } 14 | & > ul { 15 | padding: 0; 16 | margin: 0; 17 | 18 | & > li { 19 | padding-bottom: 80px; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /client/app/routes/blog-posts.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | actions: { 5 | savePost(post) { 6 | post.get('tags').then(tags => { 7 | return tags.save(); 8 | }).then((tags) => { 9 | console.log(tags); 10 | return post.save(); 11 | }).then(() => { 12 | this.transitionTo('blog-posts.post-read', post); 13 | }); 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /server/models/project.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | 4 | var ProjectSchema = new Schema({ 5 | name: String, 6 | url: String, 7 | description: String, 8 | githubID: Number, 9 | dateCreated: Date, 10 | dateUpdated: Date, 11 | stars: Number, 12 | isOwner: Boolean, 13 | isPublished: Boolean, 14 | posts: [{ type: Schema.Types.ObjectId, ref: 'Post' }] 15 | }); 16 | 17 | module.exports = mongoose.model('Project', ProjectSchema); 18 | -------------------------------------------------------------------------------- /client/.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "4" 5 | 6 | sudo: false 7 | 8 | cache: 9 | directories: 10 | - $HOME/.npm 11 | - $HOME/.cache # includes bowers cache 12 | 13 | before_install: 14 | - npm config set spin false 15 | - npm install -g bower 16 | - bower --version 17 | - npm install phantomjs-prebuilt 18 | - node_modules/phantomjs-prebuilt/bin/phantomjs --version 19 | 20 | install: 21 | - npm install 22 | - bower install 23 | 24 | script: 25 | - npm test 26 | -------------------------------------------------------------------------------- /client/app/styles/components/_autocomplete-input.scss: -------------------------------------------------------------------------------- 1 | .autocomplete { 2 | .selected-value, .found-item { 3 | @include tag-style; 4 | } 5 | input { 6 | margin-top: 10px; 7 | margin-bottom: 10px; 8 | } 9 | .autocomplete-results { 10 | list-style: none; 11 | margin: 0; 12 | padding: 0; 13 | 14 | li { 15 | display: inline-block; 16 | margin-bottom: 13px; 17 | } 18 | } 19 | 20 | margin-bottom: 20px; 21 | } 22 | -------------------------------------------------------------------------------- /client/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kuzzmi-blog", 3 | "dependencies": { 4 | "ember": "~2.10.0", 5 | "ember-cli-shims": "^0.1.3", 6 | "jquery": "^1.11.3", 7 | "moment": ">= 2.8.0", 8 | "moment-timezone": ">= 0.1.0", 9 | "highlightjs": "~8.9.1", 10 | "codemirror": "~5.8.0", 11 | "blueimp-md5": "~1.1.1", 12 | "ember-simple-auth": "0.8.0", 13 | "font-awesome": "~4.4.0", 14 | "pace": "~1.0.2", 15 | "blob-polyfill": "^1.0.20150320", 16 | "plupload": "v2.1.8" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Application from '../../app'; 3 | import config from '../../config/environment'; 4 | 5 | export default function startApp(attrs) { 6 | let application; 7 | 8 | // use defaults, but you can override 9 | let attributes = Ember.assign({}, config.APP, attrs); 10 | 11 | Ember.run(() => { 12 | application = Application.create(attributes); 13 | application.setupForTesting(); 14 | application.injectTestHelpers(); 15 | }); 16 | 17 | return application; 18 | } 19 | -------------------------------------------------------------------------------- /server/api/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | var posts = require('./posts'); 5 | var tags = require('./tags'); 6 | var users = require('./users'); 7 | var projects = require('./projects'); 8 | var auth = require('../auth'); 9 | var feed = require('./feed'); 10 | 11 | router.use('/posts', posts); 12 | router.use('/tags', tags); 13 | router.use('/users', users); 14 | router.use('/projects', projects); 15 | router.use('/auth', auth); 16 | router.use('/feed', feed); 17 | 18 | module.exports = router; 19 | -------------------------------------------------------------------------------- /client/app/styles/components/_admin-toolbar.scss: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | display: inline-block; 3 | float: right; 4 | 5 | @include max-screen(768px) { 6 | display: block; 7 | float: none; 8 | margin-bottom: 10px; 9 | } 10 | 11 | ul { 12 | margin: 0; 13 | padding: 0; 14 | list-style: none; 15 | li { 16 | font-weight: 500; 17 | display: inline-block; 18 | 19 | a { 20 | color: $grey-dark; 21 | } 22 | } 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /client/app/helpers/output-value.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export function outputValue(params) { 4 | let object = params[0], 5 | key = params[1]; 6 | 7 | if (typeof object === 'object' && typeof key === 'string') { 8 | if (object.get) { 9 | return object.get(key); 10 | } else { 11 | return object[key]; 12 | } 13 | } else { 14 | throw new TypeError('outputValue helper signature is outputValue(Object, String)'); 15 | } 16 | } 17 | 18 | export default Ember.Helper.helper(outputValue); 19 | -------------------------------------------------------------------------------- /client/app/initializers/scroll-reset.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | var initialized = false; 4 | 5 | export function initialize() { 6 | if (initialized) { 7 | return; 8 | } 9 | 10 | Ember.Route.reopen({ 11 | actions: { 12 | didTransition() { 13 | window.scrollTo(0,0); 14 | this._super(); 15 | return true; 16 | } 17 | } 18 | }); 19 | 20 | initialized = true; 21 | } 22 | 23 | export default { 24 | name: 'scroll-reset', 25 | initialize 26 | }; 27 | 28 | -------------------------------------------------------------------------------- /client/app/routes/projects/list.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | api: Ember.inject.service('api'), 5 | 6 | model() { 7 | return this.store.findAll('project'); 8 | }, 9 | 10 | actions: { 11 | sync() { 12 | this.get('api').call(true, 'projects/sync', data => { 13 | console.log(data); 14 | }); 15 | }, 16 | 17 | publish(project) { 18 | project.set('isPublished', true); 19 | project.save(); 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /client/app/routes/blog-posts/posts-list-page.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | controllerName: 'blog-posts', 5 | templateName: 'blog-posts.posts-list', 6 | 7 | model(params) { 8 | if (params.page === '1') { 9 | return this.transitionTo('blog-posts.posts-list'); 10 | } 11 | let ctrl = this.controllerFor('blog-posts'); 12 | let query = { 13 | page: params.page, 14 | size: ctrl.get('pageSize') 15 | }; 16 | ctrl.set('currentPage', params.page); 17 | return this.store.query('post', query); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /client/app/routes/projects/create.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; 3 | 4 | export default Ember.Route.extend(AuthenticatedRouteMixin, { 5 | controllerName: 'projects', 6 | model() { 7 | return this.store.createRecord('project'); 8 | }, 9 | 10 | actions: { 11 | willTransition() { 12 | var model = this.currentModel; 13 | if (model.get('hasDirtyAttributes')) { 14 | model.rollbackAttributes(); 15 | } 16 | this.store.unloadRecord(model); 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /client/app/templates/auth/login.hbs: -------------------------------------------------------------------------------- 1 | {{title "Login"}} 2 | 3 |
4 |

5 | Login 6 |

7 |

Registration is closed. Sorry.

8 | {{#if errorMessage}} 9 |

10 |

Error!

11 | {{errorMessage}} 12 |

13 | {{/if}} 14 |
15 | {{input id="username" value=username placeholder="enter username"}} 16 | {{input id="password" value=password placeholder="enter password" type="password"}} 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /server/api/feed/route.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | var Post = require('../../models/post'); 5 | var Tag = require('../../models/tag'); 6 | 7 | module.exports.rss = function(req, res) { 8 | Post.find({}) 9 | .sort('-dateCreated') 10 | .where('isPublished', true) 11 | .limit(20) 12 | .select('title slug dateCreated description') 13 | .exec(function(err, posts) { 14 | if (err) return next(err); 15 | console.log(posts); 16 | return res.render('rss', { 17 | posts: posts 18 | }); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /client/tests/integration/components/blog-post-body-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('blog-post-body', 'Integration | Component | blog post body', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | this.set('html', '

test

'); 10 | 11 | this.render(hbs` 12 | {{#blog-post-body}} 13 | {{{html}}} 14 | {{/blog-post-body}} 15 | `); 16 | 17 | assert.equal(this.$().text().trim(), 'test'); 18 | assert.equal(this.$().find('.post-body').html().trim(), '

test

'); 19 | }); 20 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise" 6 | ], 7 | "browser": true, 8 | "boss": true, 9 | "curly": true, 10 | "debug": false, 11 | "devel": true, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": false, 16 | "laxbreak": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": false, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": true, 27 | "strict": false, 28 | "white": false, 29 | "eqnull": true, 30 | "esnext": true, 31 | "unused": true 32 | } 33 | -------------------------------------------------------------------------------- /client/app/initializers/reset-navbar.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | var initialized = false; 4 | 5 | export function initialize() { 6 | if (initialized) { 7 | return; 8 | } 9 | 10 | Ember.Route.reopen({ 11 | actions: { 12 | didTransition() { 13 | Ember.$('.navbar').removeClass('show'); 14 | Ember.$('.nav-toggle').removeClass('active'); 15 | this._super(); 16 | return true; 17 | } 18 | } 19 | }); 20 | 21 | initialized = true; 22 | } 23 | 24 | export default { 25 | name: 'reset-navbar', 26 | initialize 27 | }; 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /client/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise" 6 | ], 7 | "browser": true, 8 | "boss": true, 9 | "curly": true, 10 | "debug": false, 11 | "devel": true, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": false, 16 | "laxbreak": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": false, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": true, 27 | "strict": false, 28 | "white": false, 29 | "eqnull": true, 30 | "esversion": 6, 31 | "unused": true 32 | } 33 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.js] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.hbs] 20 | insert_final_newline = false 21 | indent_style = space 22 | indent_size = 2 23 | 24 | [*.css] 25 | indent_style = space 26 | indent_size = 2 27 | 28 | [*.html] 29 | indent_style = space 30 | indent_size = 2 31 | 32 | [*.{diff,md}] 33 | trim_trailing_whitespace = false 34 | -------------------------------------------------------------------------------- /client/app/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import '_bits'; 2 | @import '_mixins'; 3 | @import '_media-queries'; 4 | @import '_helpers'; 5 | @import '_generic'; 6 | @import '_application'; 7 | @import '_projects'; 8 | @import '_about'; 9 | @import '_a2a'; 10 | @import 'auth/_login'; 11 | @import 'blog-posts/_posts-list'; 12 | @import 'blog-posts/_post-read'; 13 | @import 'blog-posts/_post-new'; 14 | @import 'components/_nav-bar'; 15 | @import 'components/_lctv-frame'; 16 | @import 'components/_tags-list'; 17 | @import 'components/_admin-toolbar'; 18 | @import 'components/_image-uploader'; 19 | @import 'components/_post-edit-form'; 20 | @import 'components/_blog-post-body'; 21 | @import 'components/_autocomplete-input'; 22 | -------------------------------------------------------------------------------- /client/tests/unit/initializers/nav-indicator-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import NavIndicatorInitializer from '../../../initializers/nav-indicator'; 3 | import { module, test } from 'qunit'; 4 | 5 | let application; 6 | 7 | module('Unit | Initializer | nav indicator', { 8 | beforeEach() { 9 | Ember.run(function() { 10 | application = Ember.Application.create(); 11 | application.deferReadiness(); 12 | }); 13 | } 14 | }); 15 | 16 | // Replace this with your real tests. 17 | test('it works', function(assert) { 18 | NavIndicatorInitializer.initialize(application); 19 | 20 | // you would normally confirm the results of the initializer here 21 | assert.ok(true); 22 | }); 23 | -------------------------------------------------------------------------------- /client/tests/unit/initializers/webfont-loader-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import WebfontLoaderInitializer from '../../../initializers/webfont-loader'; 3 | import { module, test } from 'qunit'; 4 | 5 | let application; 6 | 7 | module('Unit | Initializer | webfont loader', { 8 | beforeEach() { 9 | Ember.run(function() { 10 | application = Ember.Application.create(); 11 | application.deferReadiness(); 12 | }); 13 | } 14 | }); 15 | 16 | // Replace this with your real tests. 17 | test('it works', function(assert) { 18 | WebfontLoaderInitializer.initialize(application); 19 | 20 | // you would normally confirm the results of the initializer here 21 | assert.ok(true); 22 | }); 23 | -------------------------------------------------------------------------------- /client/tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import Ember from 'ember'; 3 | import startApp from '../helpers/start-app'; 4 | import destroyApp from '../helpers/destroy-app'; 5 | 6 | const { RSVP: { Promise } } = Ember; 7 | 8 | export default function(name, options = {}) { 9 | module(name, { 10 | beforeEach() { 11 | this.application = startApp(); 12 | 13 | if (options.beforeEach) { 14 | return options.beforeEach.apply(this, arguments); 15 | } 16 | }, 17 | 18 | afterEach() { 19 | let afterEach = options.afterEach && options.afterEach.apply(this, arguments); 20 | return Promise.resolve(afterEach).then(() => destroyApp(this.application)); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /server/auth/local/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'); 4 | var passport = require('passport'); 5 | var config = require('../../config/config'); 6 | var auth = require('../service'); 7 | 8 | var router = express.Router(); 9 | 10 | router.post('/', function(req, res, next) { 11 | passport.authenticate('local', function (err, user, info) { 12 | var error = err || info; 13 | if (error) return res.status(401).json(error); 14 | if (!user) return res.status(500).json({message: 'Something went wrong, please try again.'}); 15 | 16 | var token = auth.signToken(user._id, user.role); 17 | console.log(token); 18 | res.json({access_token: token, expiresIn: config.tokenExpiration}); 19 | })(req, res, next); 20 | }); 21 | 22 | module.exports = router; 23 | -------------------------------------------------------------------------------- /client/app/templates/components/autocomplete-input.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if items}} 3 | {{#each items as |item|}} 4 | {{output-value item key}} 5 | {{/each}} 6 | {{/if}} 7 | {{#if item}} 8 | {{output-value item key}} 9 | {{/if}} 10 | {{#if item}} 11 | {{else}} 12 | {{input type="text" placeholder="start typing" value=newItem type=text key-up="keyUp"}} 13 | {{/if}} 14 | 21 |
22 | -------------------------------------------------------------------------------- /client/tests/integration/components/lctv-frame-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('lctv-frame', 'Integration | Component | lctv frame', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... });" + EOL + EOL + 12 | 13 | this.render(hbs`{{lctv-frame}}`); 14 | 15 | assert.equal(this.$().text().trim(), ''); 16 | 17 | // Template block usage:" + EOL + 18 | this.render(hbs` 19 | {{#lctv-frame}} 20 | template block text 21 | {{/lctv-frame}} 22 | `); 23 | 24 | assert.equal(this.$().text().trim(), 'template block text'); 25 | }); 26 | -------------------------------------------------------------------------------- /client/tests/integration/components/tags-list-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('tags-list', 'Integration | Component | tags list', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | // Handle any actions with this.on('myAction', function(val) { ... });" + EOL + EOL + 10 | 11 | this.render(hbs`{{tags-list}}`); 12 | 13 | assert.equal(this.$().text().trim(), ''); 14 | }); 15 | 16 | test('it renders the same amount of links as amount of items passed as items property', function(assert) { 17 | this.set('items', [{ name: 'a' }, { name: 'b' }]); 18 | 19 | this.render(hbs`{{tags-list items=items}}`); 20 | 21 | assert.equal(this.$().text().trim(), 'a b'); 22 | }); 23 | -------------------------------------------------------------------------------- /client/app/templates/components/image-uploader.hbs: -------------------------------------------------------------------------------- 1 | {{#pl-uploader 2 | for="upload-image" 3 | extensions="jpg jpeg png gif" 4 | onfileadd="uploadImage" as |queue features|}} 5 |
6 | 7 |

8 | {{#if features.drag-and-drop}} 9 | Drag and drop images onto this area to upload them 10 | {{/if}} 11 |

12 |
13 |
14 | {{#if queue.length}} 15 | Uploading {{queue.length}} files. ({{queue.progress}}%) 16 | {{/if}} 17 | {{/pl-uploader}} 18 | 28 | -------------------------------------------------------------------------------- /client/app/components/nav-bar.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | session: Ember.inject.service('session'), 5 | lctv: Ember.inject.service(), 6 | 7 | // When I want to include the info about LCTV 8 | // didInsertElement() { 9 | // this.get('lctv').checkStatus(); 10 | // this._super(...arguments); 11 | // }, 12 | 13 | isLiveChanged: Ember.observer('lctv.isLive', function() { 14 | this.set('isLive', this.get('lctv.isLive')); 15 | }), 16 | 17 | actions: { 18 | toggleNavbar() { 19 | Ember.$('.navbar').toggleClass('show'); 20 | Ember.$('.nav-toggle').toggleClass('active'); 21 | }, 22 | 23 | invalidateSession() { 24 | this.get('session').invalidate(); 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /client/tests/integration/components/image-uploader-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('image-uploader', 'Integration | Component | image uploader', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... });" + EOL + EOL + 12 | 13 | this.render(hbs`{{image-uploader}}`); 14 | 15 | assert.equal(this.$().text().trim(), ''); 16 | 17 | // Template block usage:" + EOL + 18 | this.render(hbs` 19 | {{#image-uploader}} 20 | template block text 21 | {{/image-uploader}} 22 | `); 23 | 24 | assert.equal(this.$().text().trim(), 'template block text'); 25 | }); 26 | -------------------------------------------------------------------------------- /client/app/routes/blog-posts/post-edit.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; 3 | 4 | export default Ember.Route.extend(AuthenticatedRouteMixin, { 5 | controllerName: 'blog-posts', 6 | 7 | model(params) { 8 | return this.store.findRecord('post', params.slug); 9 | }, 10 | 11 | actions: { 12 | willTransition(transition) { 13 | var model = this.currentModel; 14 | console.log(model.get('hasDirtyAttributes')); 15 | if (model.get('hasDirtyAttributes')) { 16 | if(confirm('Are you sure?')) { 17 | model.rollbackAttributes(); 18 | } else { 19 | transition.abort(); 20 | } 21 | } 22 | } 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /server/api/projects/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var projects = require('./route'); 4 | var auth = require('../../auth/service'); 5 | var CronJob = require('cron').CronJob; 6 | 7 | router.get('/sync', auth.hasRole('admin'), projects.sync); 8 | router.post('/', auth.hasRole('admin'), projects.add); 9 | router.get('/', auth.hasRoleNotStrict('admin'), projects.getAll); 10 | router.put('/:id', auth.hasRole('admin'), projects.update); 11 | router.get('/:id', auth.hasRoleNotStrict('admin'), projects.getOne); 12 | // router.delete('/:id', auth.hasRole('admin'), projects.delete); 13 | // 14 | // Setting up a "cron job" to sync projects once in a while 15 | var job = new CronJob({ 16 | cronTime: '00 00 * * * *', 17 | onTick: projects.sync, 18 | start: true 19 | }); 20 | 21 | module.exports = router; 22 | -------------------------------------------------------------------------------- /client/tests/integration/components/project-edit-form-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('project-edit-form', 'Integration | Component | project edit form', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... });" + EOL + EOL + 12 | 13 | this.render(hbs`{{project-edit-form}}`); 14 | 15 | assert.equal(this.$().text().trim(), ''); 16 | 17 | // Template block usage:" + EOL + 18 | this.render(hbs` 19 | {{#project-edit-form}} 20 | template block text 21 | {{/project-edit-form}} 22 | `); 23 | 24 | assert.equal(this.$().text().trim(), 'template block text'); 25 | }); 26 | -------------------------------------------------------------------------------- /client/tests/unit/helpers/output-value-test.js: -------------------------------------------------------------------------------- 1 | import { outputValue } from '../../../helpers/output-value'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Helper | output value'); 5 | 6 | test('it works', function(assert) { 7 | let result = outputValue([ { foo: 'bar' }, 'foo' ]); 8 | assert.ok(result); 9 | }); 10 | 11 | test('it returns a value of object by key name', function(assert) { 12 | let result = outputValue([ { foo: 'bar' }, 'foo' ]); 13 | assert.equal(result, 'bar'); 14 | }); 15 | 16 | test('it returns undefined if object has no key passed as a second argument', function(assert) { 17 | let result = outputValue([ { foo: 'bar' }, 'bar' ]); 18 | assert.equal(result, undefined); 19 | }); 20 | 21 | test('it throws error if the signature is not matching', function(assert) { 22 | assert.throws(outputValue); 23 | }); 24 | -------------------------------------------------------------------------------- /client/tests/integration/components/autocomplete-input-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | moduleForComponent('autocomplete-input', 'Integration | Component | autocomplete input', { 5 | integration: true 6 | }); 7 | 8 | test('it renders', function(assert) { 9 | 10 | // Set any properties with this.set('myProperty', 'value'); 11 | // Handle any actions with this.on('myAction', function(val) { ... });" + EOL + EOL + 12 | 13 | this.render(hbs`{{autocomplete-input}}`); 14 | 15 | assert.equal(this.$().text().trim(), ''); 16 | 17 | // Template block usage:" + EOL + 18 | this.render(hbs` 19 | {{#autocomplete-input}} 20 | template block text 21 | {{/autocomplete-input}} 22 | `); 23 | 24 | assert.equal(this.$().text().trim(), 'template block text'); 25 | }); 26 | -------------------------------------------------------------------------------- /client/app/routes/blog-posts/post-new.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; 3 | 4 | export default Ember.Route.extend(AuthenticatedRouteMixin, { 5 | controllerName: 'blog-posts', 6 | 7 | model() { 8 | return this.store.createRecord('post'); 9 | }, 10 | 11 | actions: { 12 | willTransition(transition) { 13 | var model = this.currentModel; 14 | console.log(model.get('hasDirtyAttributes')); 15 | if (model.get('hasDirtyAttributes')) { 16 | if(confirm('Are you sure?')) { 17 | model.rollbackAttributes(); 18 | this.store.unloadRecord(model); 19 | } else { 20 | transition.abort(); 21 | } 22 | } 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /client/server/index.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 3 | // To use it create some files under `mocks/` 4 | // e.g. `server/mocks/ember-hamsters.js` 5 | // 6 | // module.exports = function(app) { 7 | // app.get('/ember-hamsters', function(req, res) { 8 | // res.send('hello'); 9 | // }); 10 | // }; 11 | 12 | module.exports = function(app) { 13 | var globSync = require('glob').sync; 14 | var mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require); 15 | var proxies = globSync('./proxies/**/*.js', { cwd: __dirname }).map(require); 16 | 17 | // Log proxy requests 18 | var morgan = require('morgan'); 19 | app.use(morgan('dev')); 20 | 21 | mocks.forEach(function(route) { route(app); }); 22 | proxies.forEach(function(route) { route(app); }); 23 | 24 | var express = require('express'); 25 | app.use('/uploads', express.static(__dirname + "/../../server/tmp")); 26 | }; 27 | -------------------------------------------------------------------------------- /client/app/controllers/auth/login.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | session: Ember.inject.service('session'), 5 | 6 | actions: { 7 | authenticate() { 8 | let { username, password } = this.getProperties('username', 'password'); 9 | 10 | this.get('session').authenticate('authenticator:oauth2', username, password) 11 | .catch((reason) => { 12 | let message; 13 | 14 | if (reason.error) { 15 | message = reason.error.message; 16 | } else if (reason.message) { 17 | message = reason.message; 18 | } else { 19 | message = reason; 20 | } 21 | 22 | this.set('errorMessage', message); 23 | }); 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /client/app/services/lctv.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from '../config/environment'; 3 | 4 | const LCTV_URL = 'https://www.livecoding.tv/livestreams'; 5 | const REGEXP = /(?:itemprop="author">\W+.*\W+)(.*)/g; 6 | 7 | export default Ember.Service.extend({ 8 | isLive: null, 9 | 10 | checkStatus() { 11 | function getMatches(string, regex, index) { 12 | index = index || 1; 13 | let matches = []; 14 | let match; 15 | while (match = regex.exec(string)) { 16 | matches.push(match[index]); 17 | } 18 | return matches; 19 | } 20 | 21 | if (this.get('isLive') !== true) { 22 | Ember.$.ajax(LCTV_URL).success(( data ) => { 23 | let test = getMatches(data, REGEXP); 24 | const live = test.indexOf(config.author.nickname) !== -1; 25 | this.set('isLive', live); 26 | }); 27 | } 28 | } 29 | }); 30 | 31 | -------------------------------------------------------------------------------- /client/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Igor Kuzmenko 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 26 | 27 | 28 | {{content-for "body-footer"}} 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.13.2", 10 | "composable-middleware": "^0.3.0", 11 | "compression": "^1.6.1", 12 | "cookie-parser": "~1.3.5", 13 | "cron": "^1.1.0", 14 | "crypto": "0.0.3", 15 | "debug": "^2.2.0", 16 | "express": "~4.13.1", 17 | "express-jwt": "^3.3.0", 18 | "express-session": "^1.12.1", 19 | "extend": "^3.0.0", 20 | "jade": "~1.11.0", 21 | "jsonwebtoken": "^5.4.1", 22 | "kerberos": "0.0.17", 23 | "marked": "^0.3.5", 24 | "mongoose": "^4.2.7", 25 | "morgan": "~1.6.1", 26 | "multer": "^1.1.0", 27 | "node-github": "0.0.3", 28 | "passport": "^0.3.2", 29 | "passport-google-oauth": "^0.2.0", 30 | "passport-http-bearer": "^1.0.1", 31 | "passport-jwt": "^1.2.1", 32 | "passport-local": "^1.0.0", 33 | "serve-favicon": "~2.3.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/api/auth/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var passport = require('passport'); 4 | var jwt = require('jsonwebtoken'); 5 | var config = require('../../config/config'); 6 | var User = require('../../models/user'); 7 | 8 | /* LOCAL AUTH */ 9 | 10 | router.route('/login') 11 | .post(passport.authenticate('local', { failureRedirect: '/login' }), 12 | function(req, res) { 13 | var token = jwt.sign({_id: req.user._id, role: req.user.role }, config.secret, { expiresIn: 60*60*5 }); 14 | res.json({ 'access_token': token }); 15 | }); 16 | 17 | /* GOOGLE AUTH */ 18 | 19 | router.route('/google').get(passport.authenticate('google', { scope: 'email' })); 20 | 21 | router.route('/google/callback').get( 22 | passport.authenticate('google', { failureRedirect: 'http://localhost:4200/login' }), 23 | function(req, res) { 24 | // Successful authentication, redirect home. 25 | res.redirect('http://localhost:4200/'); 26 | }); 27 | 28 | module.exports = router; 29 | -------------------------------------------------------------------------------- /client/tests/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "location", 6 | "setTimeout", 7 | "$", 8 | "-Promise", 9 | "define", 10 | "console", 11 | "visit", 12 | "exists", 13 | "fillIn", 14 | "click", 15 | "keyEvent", 16 | "triggerEvent", 17 | "find", 18 | "findWithAssert", 19 | "wait", 20 | "DS", 21 | "andThen", 22 | "currentURL", 23 | "currentPath", 24 | "currentRouteName" 25 | ], 26 | "node": false, 27 | "browser": false, 28 | "boss": true, 29 | "curly": true, 30 | "debug": false, 31 | "devel": false, 32 | "eqeqeq": true, 33 | "evil": true, 34 | "forin": false, 35 | "immed": false, 36 | "laxbreak": false, 37 | "newcap": true, 38 | "noarg": true, 39 | "noempty": false, 40 | "nonew": false, 41 | "nomen": false, 42 | "onevar": false, 43 | "plusplus": false, 44 | "regexp": false, 45 | "undef": true, 46 | "sub": true, 47 | "strict": false, 48 | "white": false, 49 | "eqnull": true, 50 | "esversion": 6, 51 | "unused": true 52 | } 53 | -------------------------------------------------------------------------------- /cache/phantom.js: -------------------------------------------------------------------------------- 1 | var page = require('webpage').create(); 2 | var system = require('system'); 3 | 4 | var lastReceived = new Date().getTime(); 5 | var requestCount = 0; 6 | var responseCount = 0; 7 | var requestIds = []; 8 | var startTime = new Date().getTime(); 9 | 10 | page.onResourceReceived = function (response) { 11 | if(requestIds.indexOf(response.id) !== -1) { 12 | lastReceived = new Date().getTime(); 13 | responseCount++; 14 | requestIds[requestIds.indexOf(response.id)] = null; 15 | } 16 | }; 17 | 18 | page.onResourceRequested = function (request) { 19 | if(requestIds.indexOf(request.id) === -1) { 20 | requestIds.push(request.id); 21 | requestCount++; 22 | } 23 | }; 24 | 25 | page.open(system.args[1], function (status) { 26 | if (status !== 'success') { 27 | console.log('Unable to access network'); 28 | phantom.exit(); 29 | } else { 30 | setTimeout(function() { 31 | var p = page.content; 32 | console.log(p); 33 | phantom.exit(); 34 | }, 1500); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /client/app/router.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from './config/environment'; 3 | import googlePageview from './mixins/google-pageview'; 4 | 5 | const Router = Ember.Router.extend(googlePageview, { 6 | location: config.locationType, 7 | rootURL: config.rootURL 8 | }); 9 | 10 | Router.map(function() { 11 | this.route('blog-posts', { path: '/' }, function() { 12 | this.route('posts-list', { path: '/' }); 13 | this.route('posts-list-page', { path: '/page/:page' }); 14 | this.route('post-edit', { path: 'blog/:slug/edit' }); 15 | this.route('post-new', { path: 'blog/new' }); 16 | this.route('post-read', { path: 'blog/:slug' }); 17 | this.route('tag-filtered', { path: 'blog/tag/:tag' }); 18 | }); 19 | this.route('about'); 20 | this.route('projects', function() { 21 | this.route('create'); 22 | this.route('list'); 23 | }); 24 | this.route('auth', function() { 25 | this.route('login'); 26 | }); 27 | this.route('404'); 28 | this.route('stream'); 29 | }); 30 | 31 | export default Router; 32 | -------------------------------------------------------------------------------- /client/app/styles/components/_post-edit-form.scss: -------------------------------------------------------------------------------- 1 | .post-edit-form { 2 | .form, .preview { 3 | display: block; 4 | } 5 | 6 | form { 7 | .editor { 8 | .CodeMirror { 9 | font-family: 'Roboto Mono', monospace; 10 | height: 900px; 11 | 12 | .CodeMirror-cursor { 13 | background-color: $text-color; 14 | } 15 | } 16 | 17 | &.fullscreen { 18 | position: absolute; 19 | max-width: 920px; 20 | margin: 0 auto; 21 | 22 | left: 10%; 23 | right: 10%; 24 | top: 5%; 25 | bottom: 5%; 26 | 27 | z-index: 1000; 28 | .label { 29 | font-weight: 900; 30 | } 31 | #editor-body { 32 | height: 85%; 33 | } 34 | .CodeMirror { 35 | height: 100%; 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client/app/components/image-uploader.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from '../config/environment'; 3 | 4 | let self; 5 | 6 | export default Ember.Component.extend({ 7 | api: Ember.inject.service(), 8 | 9 | didInsertElement() { 10 | self = this; 11 | }, 12 | 13 | actions: { 14 | action(link) { 15 | this.sendAction('action', link); 16 | }, 17 | 18 | uploadImage: (file) => { 19 | let image = { 20 | name: file.file.name 21 | }; 22 | 23 | file.read().then((url) => { 24 | image.file = url; 25 | }); 26 | 27 | const path = [config.API.host, config.API.namespace, 'posts/upload'].join('/'); 28 | 29 | self.get('api').getHeaders((headers) => { 30 | file.upload(path, { headers }).then((response) => { 31 | image.url = config.API.uploadPath + '/' + response.body[0].data; 32 | image.filename = response.body[0].data; 33 | self.sendAction('action', image); 34 | }, () => { }); 35 | }); 36 | } 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /client/app/services/user.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Service.extend({ 4 | session: Ember.inject.service('session'), 5 | api: Ember.inject.service('api'), 6 | user: null, 7 | 8 | init() { 9 | this._super(...arguments); 10 | 11 | let user = this.get('session').get('data.user'); 12 | if (user) { 13 | this.set('user', user); 14 | } else { 15 | this.getData(); 16 | } 17 | 18 | this.get('session').on('authenticationSucceeded', () => { 19 | this.getData(); 20 | }); 21 | this.get('session').on('invalidationSucceeded', () => { 22 | this.get('session').set('data.user', {}); 23 | }); 24 | }, 25 | 26 | isAdmin: Ember.computed('user', function() { 27 | const user = this.get('user'); 28 | if (user) { 29 | return user.role === 'admin'; 30 | } 31 | }), 32 | 33 | getData: function() { 34 | this.get('api').call(true, 'users/me', data => { 35 | this.set('user', data); 36 | this.get('session').set('data.user', data); 37 | }); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /client/app/templates/blog-posts/tag-filtered.hbs: -------------------------------------------------------------------------------- 1 |
2 | 31 |
32 | -------------------------------------------------------------------------------- /client/app/templates/components/nav-bar.hbs: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /client/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | KuzzmiBlog Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /client/app/components/project-edit-form.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | /* events */ 5 | didInsertElement() { 6 | var self = this; 7 | this._super(...arguments); 8 | var myTextarea = this.$('#editor-body')[0]; 9 | new CodeMirror(myTextarea, { 10 | value: this.get('post.markdown') || '', 11 | mode: 'markdown', 12 | keyMap: 'vim' 13 | }).on('change', function(editor) { 14 | var body = editor.getValue(); 15 | self.set('post.markdown', body); 16 | }); 17 | 18 | this.$('input:first').focus(); 19 | }, 20 | 21 | init() { 22 | this._super(...arguments); 23 | }, 24 | 25 | /* actions */ 26 | actions: { 27 | save(project) { 28 | if (!project.get('dateCreated')) { 29 | project.set('dateCreated', new Date()); 30 | } 31 | this.sendAction('save', project); 32 | }, 33 | 34 | edit() { 35 | Ember.$('.form').toggleClass('hidden'); 36 | Ember.$('.preview').toggleClass('hidden'); 37 | this.set('isPreview', false); 38 | } 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /client/tests/integration/components/admin-toolbar-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import Ember from 'ember'; 4 | 5 | moduleForComponent('admin-toolbar', 'Integration | Component | admin toolbar', { 6 | integration: true 7 | }); 8 | 9 | test('it renders content if user is admin', function(assert) { 10 | let userService = Ember.Service.extend({ 11 | isAdmin: true 12 | }); 13 | this.register('service:user', userService); 14 | this.inject.service('user', { as: 'user' }); 15 | 16 | this.render(hbs` 17 | {{#admin-toolbar}} 18 |
  • Test
  • 19 | {{/admin-toolbar}} 20 | `); 21 | 22 | assert.equal(this.$().text().trim(), 'Test'); 23 | }); 24 | 25 | test('it does not render content if user is not admin', function(assert) { 26 | let userService = Ember.Service.extend({ 27 | isAdmin: false 28 | }); 29 | this.register('service:user', userService); 30 | this.inject.service('user', { as: 'user' }); 31 | 32 | this.render(hbs` 33 | {{#admin-toolbar}} 34 |
  • Test
  • 35 | {{/admin-toolbar}} 36 | `); 37 | 38 | assert.equal(this.$().text().trim(), ''); 39 | }); 40 | -------------------------------------------------------------------------------- /client/config/deploy.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(deployTarget) { 4 | var ENV = { 5 | build: {} 6 | // include other plugin configuration that applies to all deploy targets here 7 | }; 8 | 9 | if (deployTarget === 'development') { 10 | ENV.build.environment = 'development'; 11 | // configure other plugins for development deploy target here 12 | } 13 | 14 | if (deployTarget === 'staging') { 15 | ENV.build.environment = 'production'; 16 | ENV.store = { 17 | type: 'ssh', 18 | // remoteDir: '/root/tmpw', 19 | remoteDir: '/tmp/', 20 | host: 'beta.kuzzmi.com', 21 | username: '', 22 | privateKeyFile: process.env.SSH_KEY_FILE, 23 | passphrase: '' 24 | }; 25 | // configure other plugins for staging deploy target here 26 | } 27 | 28 | if (deployTarget === 'production') { 29 | ENV.build.environment = 'production'; 30 | ENV.build.outputPath = 'dist'; 31 | // configure other plugins for production deploy target here 32 | } 33 | 34 | // Note: if you need to build some configuration asynchronously, you can return 35 | // a promise that resolves with the ENV object instead of returning the 36 | // ENV object synchronously. 37 | return ENV; 38 | }; 39 | -------------------------------------------------------------------------------- /server/api/posts/index.js: -------------------------------------------------------------------------------- 1 | var marked = require('marked'); 2 | var express = require('express'); 3 | var router = express.Router(); 4 | var posts = require('./route'); 5 | var auth = require('../../auth/service'); 6 | var config = require('../../config/config'); 7 | var multer = require('multer'); 8 | var upload = multer({ dest: config.uploadPath }); 9 | 10 | router.post('/', auth.hasRole('admin'), posts.add); 11 | router.get('/count', posts.getCount); 12 | router.get('/', auth.hasRoleNotStrict('admin'), posts.getAll); 13 | router.put('/:id', auth.hasRole('admin'), posts.update); 14 | router.get('/:slug', auth.hasRoleNotStrict('admin'), posts.getOne); 15 | router.delete('/:id', auth.hasRole('admin'), posts.delete); 16 | 17 | // uploads 18 | router.post('/upload', upload.single('file'), auth.hasRole('admin'), posts.upload); 19 | router.delete('/upload/:id', auth.hasRole('admin'), posts.deleteUploaded); 20 | /* 21 | * MARKDOWN PREVIEW 22 | */ 23 | router.route('/preview') 24 | .post(function(req, res) { 25 | var body = req.body.body; 26 | if (body) { 27 | body = marked(body); 28 | res.json({ 29 | 'html': body 30 | }); 31 | } else { 32 | throw 'No body specified'; 33 | } 34 | }); 35 | 36 | module.exports = router; 37 | -------------------------------------------------------------------------------- /server/views/rss.jade: -------------------------------------------------------------------------------- 1 | doctype xml 2 | rss(version='2.0', xmlns:atom='http://www.w3.org/2005/Atom') 3 | channel 4 | title Igor Kuzmenko Blog 5 | link https://kuzzmi.com 6 | atom:link(href='https://kuzzmi.com/api/feed/rss', rel='self', type='application/rss+xml') 7 | description Random frontend developer's thoughts and topics. 8 | language en-US 9 | if posts.length 10 | lastBuildDate= new Date(posts[0].dateCreated).toUTCString() 11 | each post in posts 12 | item 13 | title= post.title 14 | link https://kuzzmi.com/blog/#{post.slug} 15 | description 16 | | 20 | pubDate= new Date(post.dateCreated).toUTCString() 21 | guid(isPermaLink='true') https://kuzzmi.com/blog/#{post.slug} 22 | -------------------------------------------------------------------------------- /client/app/initializers/nav-indicator.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { WebFont } from 'webfontloader'; 3 | 4 | var initialized = false; 5 | 6 | export function initialize() { 7 | if (initialized) { 8 | return; 9 | } 10 | 11 | let areFontsLoaded = false; 12 | 13 | let setIndicator = () => { 14 | let indicator = Ember.$('nav .active-indicator')[0]; 15 | let activeLink = Ember.$('nav li a.active')[0]; 16 | if (indicator && activeLink) { 17 | indicator.style.left = activeLink.offsetLeft + 'px'; 18 | indicator.style.width = activeLink.offsetWidth + 'px'; 19 | } 20 | }; 21 | 22 | Ember.Route.reopen({ 23 | actions: { 24 | didTransition() { 25 | if (areFontsLoaded) { 26 | setTimeout(setIndicator, 1); 27 | } else { 28 | WebFont.on('active', () => { 29 | areFontsLoaded = true; 30 | setIndicator(); 31 | }, true); 32 | } 33 | this._super(); 34 | return true; 35 | } 36 | } 37 | }); 38 | 39 | // console.log(Ember.Route.prototype); 40 | 41 | initialized = true; 42 | } 43 | 44 | export default { 45 | name: 'nav-indicator', 46 | initialize 47 | }; 48 | -------------------------------------------------------------------------------- /client/app/templates/components/project-edit-form.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#admin-toolbar}} 3 |
  • 4 | [F2] Save 5 |
  • 6 |
  • 7 | {{#if project.id}} 8 | {{link-to "[F10] Close" "blog-posts.post-read" post}} 9 | {{else}} 10 | {{link-to "[F10] Close" "blog-posts.posts-list"}} 11 | {{/if}} 12 |
  • 13 | {{/admin-toolbar}} 14 |

    15 | {{moment-from-now project.dateCreated interval=1000}} 16 |

    17 |
    18 |

    19 | {{form-title}} 20 |

    21 |
    22 |

    Name

    23 | {{input id="name" value=project.name placeholder="project name" type="text"}} 24 |

    URL

    25 | {{input id="url" value=project.url placeholder="project url" type="text"}} 26 |

    Published

    27 | {{input id="published" type="checkbox" name="isPublished" checked=project.isPublished}} 28 |

    Description

    29 | {{textarea id="description" value=project.description placeholder="project description" type="text"}} 30 |
    31 |
    32 |
    33 | -------------------------------------------------------------------------------- /client/app/styles/_application.scss: -------------------------------------------------------------------------------- 1 | *, :after, :before { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | 6 | font-smoothing: antialiased; 7 | text-rendering: optimizeLegibility; 8 | } 9 | 10 | .pace .pace-progress { 11 | background: $accent; 12 | } 13 | 14 | html, 15 | body { 16 | font-family: 'Roboto Mono', monospace; 17 | margin: 0; 18 | padding: 0; 19 | width: 100%; 20 | min-height: 100%; 21 | color: $text-color; 22 | overflow-x: hidden; 23 | 24 | @include max-screen(768px) { 25 | font-size: 100%; 26 | } 27 | 28 | &.wf-loading { 29 | font-size: 17px; 30 | font-family: monospace; 31 | line-height: 23px; 32 | } 33 | } 34 | 35 | .wrapper { 36 | display: block; 37 | margin-right: auto; 38 | margin-left: auto; 39 | max-width: 1000px; 40 | padding-left: 0; 41 | padding-right: 0; 42 | 43 | .content { 44 | padding: 20px 40px; 45 | padding-top: 0px; 46 | 47 | @include max-screen(768px) { 48 | padding: 20px; 49 | padding-top: 0px; 50 | } 51 | 52 | .footer { 53 | margin: 40px 0; 54 | text-align: center; 55 | .heart { 56 | color: red; 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client/tests/integration/components/nav-bar-test.js: -------------------------------------------------------------------------------- 1 | import { moduleForComponent, test } from 'ember-qunit'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | import Ember from 'ember'; 4 | 5 | moduleForComponent('nav-bar', 'Integration | Component | nav bar', { 6 | integration: true 7 | }); 8 | 9 | test('it renders four li elements', function(assert) { 10 | 11 | // Template block usage:" + EOL + 12 | this.render(hbs`{{nav-bar}}`); 13 | 14 | assert.equal(this.$().find('li').length, 4); 15 | }); 16 | 17 | test('it renders Login link if user is not authenticated', function(assert) { 18 | let sessionService = Ember.Service.extend({ 19 | isAuthenticated: false 20 | }); 21 | this.register('service:session', sessionService); 22 | this.inject.service('session', { as: 'session' }); 23 | 24 | this.render(hbs`{{nav-bar}}`); 25 | 26 | assert.equal(this.$().find('li').last().text().trim(), 'Login'); 27 | }); 28 | 29 | test('it renders Logout link if user is authenticated', function(assert) { 30 | let sessionService = Ember.Service.extend({ 31 | isAuthenticated: true 32 | }); 33 | this.register('service:session', sessionService); 34 | this.inject.service('session', { as: 'session' }); 35 | 36 | this.render(hbs`{{nav-bar}}`); 37 | 38 | assert.equal(this.$().find('li').last().text().trim(), 'Logout'); 39 | }); 40 | -------------------------------------------------------------------------------- /client/app/services/api.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from '../config/environment'; 3 | 4 | export default Ember.Service.extend({ 5 | session: Ember.inject.service('session'), 6 | 7 | call(toAuthorize, endpoint, options, success, error) { 8 | if (typeof options === 'function') { 9 | error = success; 10 | success = options; 11 | options = null; 12 | } 13 | 14 | function callFn(headers) { 15 | options = options || {}; 16 | headers = headers || {}; 17 | options.headers = headers; 18 | 19 | success = success || ( () => {} ); 20 | error = error || ( () => {} ); 21 | let url = [config.API.host, config.API.namespace, endpoint].join('/'); 22 | 23 | Ember.$.ajax(url, options).success(success).error(error); 24 | } 25 | 26 | if (typeof toAuthorize === 'boolean' && toAuthorize === true) { 27 | this.getHeaders(callFn); 28 | } else { 29 | callFn(); 30 | } 31 | }, 32 | 33 | getHeaders(callFn) { 34 | let session = this.get('session'); 35 | session.authorize('authorizer:oauth2', (headerName, headerValue) => { 36 | const headers = {}; 37 | headers[headerName] = headerValue; 38 | callFn(headers); 39 | }); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /client/ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | /* global require, module */ 3 | var EmberApp = require('ember-cli/lib/broccoli/ember-app'); 4 | 5 | module.exports = function(defaults) { 6 | var app = new EmberApp(defaults, { 7 | // Add options here 8 | }); 9 | 10 | // Use `app.import` to add additional libraries to the generated 11 | // output files. 12 | // 13 | // If you need to use different assets in different 14 | // environments, specify an object as the first parameter. That 15 | // object's keys should be the environment name and the values 16 | // should be the asset to use in that environment. 17 | // 18 | // If the library that you are including contains AMD or ES6 19 | // modules that you would like to import into your application 20 | // please specify an object with the list of modules as keys 21 | // along with the exports of each module as its value. 22 | app.import('bower_components/highlightjs/styles/dark.css'); 23 | app.import('bower_components/highlightjs/highlight.pack.js'); 24 | app.import('bower_components/codemirror/lib/codemirror.js'); 25 | app.import('bower_components/codemirror/keymap/vim.js'); 26 | app.import('bower_components/codemirror/mode/markdown/markdown.js'); 27 | app.import('bower_components/codemirror/lib/codemirror.css'); 28 | app.import('bower_components/codemirror/theme/hopscotch.css'); 29 | 30 | return app.toTree(); 31 | }; 32 | -------------------------------------------------------------------------------- /client/app/templates/blog-posts/posts-list.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#admin-toolbar}} 3 |
  • 4 | {{link-to "[F4] New post" "blog-posts.post-new"}} 5 |
  • 6 | {{/admin-toolbar}} 7 | 8 | 32 | 42 |
    43 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # kuzzmi-blog 2 | 3 | This README outlines the details of collaborating on this Ember application. 4 | A short introduction of this app could easily go here. 5 | 6 | ## Prerequisites 7 | 8 | You will need the following things properly installed on your computer. 9 | 10 | * [Git](https://git-scm.com/) 11 | * [Node.js](https://nodejs.org/) (with NPM) 12 | * [Bower](https://bower.io/) 13 | * [Ember CLI](https://ember-cli.com/) 14 | * [PhantomJS](http://phantomjs.org/) 15 | 16 | ## Installation 17 | 18 | * `git clone ` this repository 19 | * `cd kuzzmi-blog` 20 | * `npm install` 21 | * `bower install` 22 | 23 | ## Running / Development 24 | 25 | * `ember serve` 26 | * Visit your app at [http://localhost:4200](http://localhost:4200). 27 | 28 | ### Code Generators 29 | 30 | Make use of the many generators for code, try `ember help generate` for more details 31 | 32 | ### Running Tests 33 | 34 | * `ember test` 35 | * `ember test --server` 36 | 37 | ### Building 38 | 39 | * `ember build` (development) 40 | * `ember build --environment production` (production) 41 | 42 | ### Deploying 43 | 44 | Specify what it takes to deploy your app. 45 | 46 | ## Further Reading / Useful Links 47 | 48 | * [ember.js](http://emberjs.com/) 49 | * [ember-cli](https://ember-cli.com/) 50 | * Development Browser Extensions 51 | * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) 52 | * [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) 53 | -------------------------------------------------------------------------------- /client/app/templates/about.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{gravatar-image email=model.email size="202"}} 3 |

    4 | Hello, my name is {{model.name}}. I love my wife, JavaScript, and Vim. 5 |

    6 |

    7 | I'm somewhat social: 8 |

    22 |

    23 |

    24 | I do: 25 |

    36 | 37 | I do not: 38 | 43 |

    44 |
    45 | -------------------------------------------------------------------------------- /client/app/templates/blog-posts/post-read.hbs: -------------------------------------------------------------------------------- 1 | {{title model.title}} 2 | 3 |
    4 |
    5 | 6 | 7 | 8 | 9 | 10 | 11 |
    12 | {{#admin-toolbar}} 13 |
  • 14 | {{link-to "[F4] Edit" "blog-posts.post-edit" model}} 15 |
  • 16 |
  • 17 | [F8] Delete 18 |
  • 19 | {{/admin-toolbar}} 20 |

    21 | {{if model.isPublished "" "[ DRAFT ]"}} 22 | {{moment-format model.dateCreated "DD/MM/YY HH:mm"}} 23 |

    24 |

    25 | {{model.title}} 26 |

    27 | {{#blog-post-body}} 28 | {{{model.body}}} 29 | {{/blog-post-body}} 30 | {{tags-list tags=model.tags}} 31 |
    32 | 33 | 34 | 35 | 36 | 37 | 38 |
    39 |
    40 |

    Comments

    41 | {{disqus-comments identifier=model.id title=model.title}} 42 |
    43 |
    44 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kuzzmi-blog", 3 | "version": "0.0.0", 4 | "description": "Small description for kuzzmi-blog goes here", 5 | "license": "MIT", 6 | "author": "", 7 | "directories": { 8 | "doc": "doc", 9 | "test": "tests" 10 | }, 11 | "repository": "", 12 | "scripts": { 13 | "build": "ember build", 14 | "start": "ember server", 15 | "test": "ember test" 16 | }, 17 | "devDependencies": { 18 | "broccoli-asset-rev": "^2.4.5", 19 | "ember-ajax": "^2.4.1", 20 | "ember-cli": "2.10.0", 21 | "ember-cli-app-version": "^2.0.0", 22 | "ember-cli-babel": "^5.1.7", 23 | "ember-cli-dependency-checker": "^1.3.0", 24 | "ember-cli-font-awesome": "1.5.2", 25 | "ember-cli-google-analytics": "1.5.0", 26 | "ember-cli-gravatar": "3.7.1", 27 | "ember-cli-htmlbars": "1.1.1", 28 | "ember-cli-htmlbars-inline-precompile": "^0.3.3", 29 | "ember-cli-inject-live-reload": "^1.4.1", 30 | "ember-cli-jshint": "^2.0.1", 31 | "ember-cli-meta-tags": "3.0.4", 32 | "ember-cli-moment-shim": "3.0.0", 33 | "ember-cli-qunit": "^3.0.1", 34 | "ember-cli-release": "^0.2.9", 35 | "ember-cli-sass": "5.6.0", 36 | "ember-cli-sri": "^2.1.0", 37 | "ember-cli-test-loader": "^1.1.0", 38 | "ember-cli-uglify": "^1.2.0", 39 | "ember-cli-webfontloader": "0.3.1", 40 | "ember-data": "^2.10.0", 41 | "ember-disqus": "0.2.0", 42 | "ember-export-application-global": "^1.0.5", 43 | "ember-load-initializers": "^0.5.1", 44 | "ember-local-storage": "1.3.3", 45 | "ember-moment": "7.0.3", 46 | "ember-page-title": "3.0.10", 47 | "ember-plupload": "1.13.18", 48 | "ember-resolver": "^2.0.3", 49 | "ember-simple-auth": "1.1.0", 50 | "ember-welcome-page": "^1.0.3", 51 | "loader.js": "^4.0.10", 52 | "webfontloader": "1.6.27" 53 | }, 54 | "engines": { 55 | "node": ">= 0.12.0" 56 | }, 57 | "private": true 58 | } 59 | -------------------------------------------------------------------------------- /client/tests/unit/services/user-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { moduleFor, test } from 'ember-qunit'; 3 | 4 | moduleFor('service:user', 'Unit | Service | user', { 5 | // Specify the other units that are required for this test. 6 | // needs: ['service:foo'] 7 | beforeEach() { 8 | 9 | let sessionService = Ember.Service.extend({ 10 | isAuthenticated: false, 11 | authorize(callback, isAdmin) { 12 | Ember.set(this, 'isAuthenticated', true); 13 | if (typeof callback === 'function') { 14 | callback({ 15 | role: isAdmin ? 'admin' : 'user' 16 | }); 17 | } 18 | } 19 | }); 20 | this.register('service:session', sessionService); 21 | this.inject.service('session', { as: 'session' }); 22 | 23 | } 24 | }); 25 | 26 | // Replace this with your real tests. 27 | test('it exists', function(assert) { 28 | let service = this.subject(); 29 | assert.ok(service); 30 | }); 31 | 32 | test('it should set user object, if session gets authenticated', function(assert) { 33 | let service = this.subject(); 34 | service.get('session').authorize(user => { 35 | service.set('user', user); 36 | }, true); 37 | assert.ok(service.get('user')); 38 | }); 39 | 40 | test('it should return isAdmin === true if user object has role "admin"', function(assert) { 41 | let service = this.subject(); 42 | service.get('session').authorize(user => { 43 | service.set('user', user); 44 | }, true); 45 | assert.equal(service.get('isAdmin'), true); 46 | }); 47 | 48 | test('it should return isAdmin === false if user object has role other than "admin"', function(assert) { 49 | let service = this.subject(); 50 | service.get('session').authorize(user => { 51 | service.set('user', user); 52 | }, false); 53 | assert.equal(service.get('isAdmin'), false); 54 | }); 55 | -------------------------------------------------------------------------------- /server/api/tags/route.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var extend = require('extend'); 3 | var Tag = require('../../models/tag'); 4 | 5 | module.exports.add = function(req, res) { 6 | var tag = new Tag(req.body.tag); 7 | tag.save(function(err) { 8 | if (err) { 9 | res.send(err); 10 | } 11 | res.json({ 12 | tag: tag 13 | }); 14 | }); 15 | }; 16 | 17 | module.exports.getAll = function(req, res) { 18 | var query = req.query || null; 19 | console.log(query); 20 | Tag.find(query, function(err, tags) { 21 | if (err) { 22 | res.send(err); 23 | } 24 | res.json({ 25 | tags: tags 26 | }); 27 | }); 28 | 29 | }; 30 | 31 | module.exports.update = function(req, res) { 32 | var id = req.params.id; 33 | 34 | Tag.findOne({ 35 | _id: id 36 | }, function(err, tag) { 37 | if (err) { 38 | res.send(err); 39 | } 40 | 41 | extend(true, tag, req.body.tag); 42 | 43 | tag.save(function(err, tag) { 44 | if (err) { 45 | res.send(err); 46 | } 47 | res.json({ 48 | tag: tag 49 | }); 50 | }); 51 | }); 52 | }; 53 | 54 | module.exports.getOne = function(req, res) { 55 | var id = req.params.id; 56 | 57 | Tag.findOne({ 58 | _id: id 59 | }, function(err, tag) { 60 | if (err) { 61 | res.send(err); 62 | } 63 | res.json({ 64 | tag: tag 65 | }); 66 | }); 67 | }; 68 | 69 | module.exports.delete = function(req, res) { 70 | var id = req.params.id; 71 | 72 | Tag.findOne({ 73 | _id: id 74 | }, function(err, post) { 75 | if (err) { 76 | res.send(err); 77 | } 78 | post.remove(function(err) { 79 | if (err) { 80 | res.send(err); 81 | } 82 | res.json({}); 83 | }); 84 | }); 85 | }; 86 | -------------------------------------------------------------------------------- /client/app/styles/components/_blog-post-body.scss: -------------------------------------------------------------------------------- 1 | $space: 0px; 2 | @mixin outofspace { 3 | margin-left: $space; 4 | margin-right: $space 5 | } 6 | 7 | .post-body { 8 | p { 9 | padding: 10px 0; 10 | 11 | &:first-of-type { 12 | padding: 0; 13 | } 14 | 15 | code { 16 | @include max-screen(768px) { 17 | font-size: 90%; 18 | padding: 0px 4px; 19 | } 20 | 21 | font-size: 14px; 22 | padding: 2px 6px; 23 | border-radius: 2px; 24 | background: $grey-lightest; 25 | } 26 | } 27 | 28 | h1, h2, h3, h4 { 29 | margin-top: 40px; 30 | 31 | &:first-child { 32 | margin-top: 0; 33 | } 34 | } 35 | 36 | pre, blockquote { 37 | @include outofspace; 38 | @include max-screen(768px) { 39 | margin-left: -20px; 40 | margin-right: -20px; 41 | } 42 | } 43 | 44 | pre { 45 | background: #444; 46 | } 47 | 48 | code { 49 | max-width: 1000px; 50 | 51 | @include max-screen(768px) { 52 | max-width: 100%; 53 | font-size: 90%; 54 | } 55 | 56 | margin-left: auto; 57 | margin-right: auto; 58 | padding: 20px 40px; 59 | font-family: 'Roboto Mono', monospace; 60 | font-size: 14px; 61 | } 62 | 63 | blockquote { 64 | background: $grey-lightest; 65 | padding: 20px 40px; 66 | 67 | @include max-screen(768px) { 68 | padding: 20px; 69 | } 70 | 71 | p, ul, ol { 72 | max-width: 900px; 73 | margin-left: auto; 74 | margin-right: auto; 75 | } 76 | 77 | p:last-child { 78 | padding-bottom: 0; 79 | } 80 | } 81 | 82 | ul { 83 | li:before { 84 | content: "* "; 85 | } 86 | } 87 | 88 | caption { 89 | font-size: 12px; 90 | color: $grey; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /server/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('server:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port and host from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | var host = process.env.HOST; 17 | app.set('port', port); 18 | app.set('host', host); 19 | 20 | /** 21 | * Create HTTP server. 22 | */ 23 | 24 | var server = http.createServer(app); 25 | 26 | /** 27 | * Listen on provided port, if provided. 28 | */ 29 | if (host) { 30 | server.listen(port, host); 31 | } else { 32 | server.listen(port); 33 | } 34 | server.on('error', onError); 35 | server.on('listening', onListening); 36 | 37 | /** 38 | * Normalize a port into a number, string, or false. 39 | */ 40 | 41 | function normalizePort(val) { 42 | var port = parseInt(val, 10); 43 | 44 | if (isNaN(port)) { 45 | // named pipe 46 | return val; 47 | } 48 | 49 | if (port >= 0) { 50 | // port number 51 | return port; 52 | } 53 | 54 | return false; 55 | } 56 | 57 | /** 58 | * Event listener for HTTP server "error" event. 59 | */ 60 | 61 | function onError(error) { 62 | if (error.syscall !== 'listen') { 63 | throw error; 64 | } 65 | 66 | var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; 67 | 68 | // handle specific listen errors with friendly messages 69 | switch (error.code) { 70 | case 'EACCES': 71 | console.error(bind + ' requires elevated privileges'); 72 | process.exit(1); 73 | break; 74 | case 'EADDRINUSE': 75 | console.error(bind + ' is already in use'); 76 | process.exit(1); 77 | break; 78 | default: 79 | throw error; 80 | } 81 | } 82 | 83 | /** 84 | * Event listener for HTTP server "listening" event. 85 | */ 86 | 87 | function onListening() { 88 | var addr = server.address(); 89 | var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; 90 | debug('Listening on ' + bind); 91 | } 92 | -------------------------------------------------------------------------------- /cache/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('server:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port and host from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3300'); 16 | var host = process.env.HOST; 17 | app.set('port', port); 18 | app.set('host', host); 19 | 20 | /** 21 | * Create HTTP server. 22 | */ 23 | 24 | var server = http.createServer(app); 25 | 26 | /** 27 | * Listen on provided port, if provided. 28 | */ 29 | if (host) { 30 | server.listen(port, host); 31 | } else { 32 | server.listen(port); 33 | } 34 | server.on('error', onError); 35 | server.on('listening', onListening); 36 | 37 | /** 38 | * Normalize a port into a number, string, or false. 39 | */ 40 | 41 | function normalizePort(val) { 42 | var port = parseInt(val, 10); 43 | 44 | if (isNaN(port)) { 45 | // named pipe 46 | return val; 47 | } 48 | 49 | if (port >= 0) { 50 | // port number 51 | return port; 52 | } 53 | 54 | return false; 55 | } 56 | 57 | /** 58 | * Event listener for HTTP server "error" event. 59 | */ 60 | 61 | function onError(error) { 62 | if (error.syscall !== 'listen') { 63 | throw error; 64 | } 65 | 66 | var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; 67 | 68 | // handle specific listen errors with friendly messages 69 | switch (error.code) { 70 | case 'EACCES': 71 | console.error(bind + ' requires elevated privileges'); 72 | process.exit(1); 73 | break; 74 | case 'EADDRINUSE': 75 | console.error(bind + ' is already in use'); 76 | process.exit(1); 77 | break; 78 | default: 79 | throw error; 80 | } 81 | } 82 | 83 | /** 84 | * Event listener for HTTP server "listening" event. 85 | */ 86 | 87 | function onListening() { 88 | var addr = server.address(); 89 | var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; 90 | debug('Listening on ' + bind); 91 | } 92 | 93 | -------------------------------------------------------------------------------- /client/tests/integration/components/post-edit-form-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { moduleForComponent, test } from 'ember-qunit'; 3 | import hbs from 'htmlbars-inline-precompile'; 4 | 5 | moduleForComponent('post-edit-form', 'Integration | Component | post edit form', { 6 | integration: true 7 | }); 8 | 9 | test('it should show time passed since the dateCreated', function(assert) { 10 | let post = Ember.Object.create({ 11 | title: 'Title', 12 | markdown: 'Markdown', 13 | description: 'Description' 14 | }); 15 | 16 | this.set('post', post); 17 | this.set('form-title', 'Test'); 18 | this.render(hbs`{{post-edit-form post=post}}`); 19 | 20 | assert.equal(this.$('.current-time').text().trim(), 'a few seconds ago'); 21 | }); 22 | 23 | test('it should show form title', function(assert) { 24 | this.set('form-title', 'Test'); 25 | this.render(hbs`{{post-edit-form form-title=form-title}}`); 26 | 27 | assert.equal(this.$('.form-title').text().trim(), 'Test'); 28 | }); 29 | 30 | test('it should show time passed since the dateCreated', function(assert) { 31 | let post = Ember.Object.extend({ 32 | title: 'Title' 33 | }); 34 | 35 | this.set('post', post); 36 | this.render(hbs`{{post-edit-form post=post form-title=form-title}}`); 37 | 38 | assert.equal(this.$('#title').val().trim(), 'Title'); 39 | }); 40 | 41 | 42 | test('it should show time passed since the dateCreated', function(assert) { 43 | let post = Ember.Object.extend({ 44 | description: 'Description' 45 | }); 46 | 47 | this.set('post', post); 48 | this.render(hbs`{{post-edit-form post=post form-title=form-title}}`); 49 | 50 | assert.equal(this.$('#description').val().trim(), 'Description'); 51 | }); 52 | 53 | test('it should show time passed since the dateCreated', function(assert) { 54 | let post = Ember.Object.extend({ 55 | markdown: 'Markdown' 56 | }); 57 | 58 | this.set('post', post); 59 | this.render(hbs`{{post-edit-form post=post form-title=form-title}}`); 60 | 61 | assert.equal(this.$('#editor-body textarea').val().trim(), 'Markdown'); 62 | }); 63 | -------------------------------------------------------------------------------- /server/auth/local/passport.js: -------------------------------------------------------------------------------- 1 | var passport = require('passport'); 2 | var LocalStrategy = require('passport-local').Strategy; 3 | 4 | var checked = false; 5 | 6 | // TODO 7 | // Add flags to prevent this route to check the Users collection 8 | // everytime, after setting up the system. 9 | 10 | exports.setup = function (User, config) { 11 | passport.use(new LocalStrategy({ 12 | // TODO: switch to emails 13 | // usernameField: 'email', 14 | usernameField: 'username', 15 | passwordField: 'password' // this is the virtual field on the model 16 | }, 17 | function(username, password, done) { 18 | User.findOne({ 19 | username: username 20 | }, function(err, user) { 21 | if (err) return done(err); 22 | 23 | if (!user) { 24 | // Refactor this to check Users only once 25 | User.find({}, function(err, users) { 26 | if (checked === false) { 27 | checked = true; 28 | if (users.length) { 29 | return done(null, false, { message: 'This user is not registered.' }); 30 | } else { 31 | var newAdmin = new User({ 32 | role: 'admin', 33 | username: username, 34 | password: password 35 | }); 36 | newAdmin.save(function(err) { 37 | if (err) { 38 | return done(null, false, { message: 'Failed to create and setup admin account' }); 39 | } 40 | return done(null, newAdmin); 41 | }); 42 | } 43 | } 44 | }) 45 | } else if (!user.authenticate(password)) { 46 | return done(null, false, { message: 'This password is not correct.' }); 47 | } else { 48 | return done(null, user); 49 | } 50 | }); 51 | })); 52 | }; 53 | -------------------------------------------------------------------------------- /client/app/controllers/blog-posts.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { storageFor } from 'ember-local-storage'; 3 | 4 | export default Ember.Controller.extend({ 5 | session: Ember.inject.service(), 6 | api: Ember.inject.service(), 7 | user: Ember.inject.service(), 8 | 9 | backup: storageFor('postBackup'), 10 | 11 | pageSize: 5, 12 | currentPage: null, 13 | postsCount: null, 14 | 15 | isNotFirstPage: Ember.computed('currentPage', function() { 16 | return ( +this.get('currentPage') !== 1 ); 17 | }), 18 | 19 | isNotLastPage: Ember.computed('currentPage', 'pageSize', 'postsCount', function() { 20 | let size = this.get('pageSize'); 21 | let cur = this.get('currentPage'); 22 | let max = this.get('postsCount') - (size * cur); 23 | return max > 0; 24 | }), 25 | 26 | nextPage: Ember.computed('currentPage', function() { 27 | return +( this.get('currentPage') ) + 1; 28 | }), 29 | 30 | prevPage: Ember.computed('currentPage', function() { 31 | return +( this.get('currentPage') ) - 1; 32 | }), 33 | 34 | isDirty: Ember.computed('model', function() { 35 | return this.get('model').get('hasDirtyAttributes'); 36 | }), 37 | 38 | init() { 39 | this.get('api').call(false, '/posts/count', (data) => { 40 | this.set('postsCount', data.count); 41 | }); 42 | 43 | Ember.$(window).on('beforeunload', () => { 44 | if (this.get('isDirty')) { 45 | this.set('backup.markdown', this.get('model.markdown')); 46 | return 'Are you sure?'; 47 | } 48 | }); 49 | }, 50 | 51 | actions: { 52 | incrPage() { 53 | let cur = this.get('currentPage'); 54 | if (!this.get('isLastPage')) { 55 | this.transitionTo('blog-posts.posts-list-page', cur++); 56 | } 57 | }, 58 | decrPage() { 59 | let cur = this.get('currentPage'); 60 | if (cur === 2) { 61 | this.transitionTo('blog-posts.posts-list', cur--); 62 | } else { 63 | this.transitionTo('blog-posts.posts-list-page', cur--); 64 | } 65 | } 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /cache/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var redis = require('redis'), 3 | client = redis.createClient(); 4 | 5 | var app = express(); 6 | 7 | var getContent = function(url, callback) { 8 | var content = ''; 9 | 10 | var phantom = require('child_process').spawn('phantomjs', ['/root/ember-express-blog/cache/phantom.js', url]); 11 | phantom.stdout.setEncoding('utf8'); 12 | phantom.stdout.on('data', function(data) { 13 | content += data.toString(); 14 | }); 15 | phantom.stderr.on('data', function(data) { 16 | console.log(data.toString()); 17 | }); 18 | phantom.on('exit', function(code) { 19 | if (code !== 0) { 20 | console.log('We have an error: ' + code); 21 | } else { 22 | callback(content); 23 | } 24 | }); 25 | }; 26 | 27 | var respond = function (req, res) { 28 | client.get(req.params[0], function(err, data) { 29 | if (!data || req.params[0] === '/') { 30 | url = 'https://kuzzmi.com' + req.params[0]; 31 | getContent(url, function (content) { 32 | client.set(req.params[0], content); 33 | res.send(content); 34 | }); 35 | } else { 36 | res.send(data); 37 | } 38 | }); 39 | }; 40 | 41 | app.get(/(.*)/, respond); 42 | 43 | // catch 404 and forward to error handler 44 | app.use(function(req, res, next) { 45 | var err = new Error('Not Found'); 46 | err.status = 404; 47 | next(err); 48 | }); 49 | 50 | // error handlers 51 | 52 | // development error handler 53 | // will print stacktrace 54 | if (app.get('env') === 'development') { 55 | app.use(function(err, req, res, next) { 56 | res.status(err.status || 500); 57 | res.json({ 58 | 'error': { 59 | message: err.message, 60 | error: err 61 | } 62 | }); 63 | }); 64 | } 65 | 66 | // production error handler 67 | // no stacktraces leaked to user 68 | app.use(function(err, req, res, next) { 69 | res.status(err.status || 500); 70 | res.json({ 71 | 'error': { 72 | message: err.message, 73 | error: {} 74 | } 75 | }); 76 | }); 77 | 78 | 79 | module.exports = app; 80 | -------------------------------------------------------------------------------- /client/server/mocks/posts.js: -------------------------------------------------------------------------------- 1 | function generatePost(id) { 2 | return { 3 | id: id, 4 | title: 'Post ' + id, 5 | body: 'Post ' + id + ' Body', 6 | description: 'This post #' + id + ' is about testing', 7 | dateCreated: new Date() 8 | }; 9 | } 10 | 11 | var data = { 12 | 'posts': [ 13 | generatePost(1), 14 | generatePost(2), 15 | generatePost(3), 16 | generatePost(4), 17 | { 18 | id: 5, 19 | title: 'Super test', 20 | description: 'Testing old post', 21 | dateCreated: new Date('2014/07/21'), 22 | body: '\ 23 |
    \
    24 | console.log("hehehe");\
    25 |             
    ' 26 | } 27 | ] 28 | }; 29 | 30 | /*jshint node:true*/ 31 | module.exports = function(app) { 32 | var express = require('express'); 33 | var postsRouter = express.Router(); 34 | 35 | postsRouter.get('/', function(req, res) { 36 | res.send(data); 37 | }); 38 | 39 | postsRouter.post('/', function(req, res) { 40 | var post = req.body.post; 41 | post.id = data.posts.length + 1; 42 | res.status(201).json({ 43 | 'posts': post 44 | }); 45 | }); 46 | 47 | postsRouter.get('/:id', function(req, res) { 48 | var postData = data.posts.filter(function(post) { 49 | return post.id === req.params.id; 50 | })[0] 51 | res.send({ 52 | 'posts': postData 53 | }); 54 | }); 55 | 56 | postsRouter.put('/:id', function(req, res) { 57 | res.send({ 58 | 'posts': { 59 | id: req.params.id 60 | } 61 | }); 62 | }); 63 | 64 | postsRouter.delete('/:id', function(req, res) { 65 | res.status(204).end(); 66 | }); 67 | 68 | // The POST and PUT call will not contain a request body 69 | // because the body-parser is not included by default. 70 | // To use req.body, run: 71 | 72 | // npm install --save-dev body-parser 73 | 74 | // After installing, you need to `use` the body-parser for 75 | // this mock uncommenting the following line: 76 | // 77 | var bodyParser = require('body-parser'); 78 | app.use(bodyParser.urlencoded({ extended: false })); 79 | app.use(bodyParser.json()); 80 | app.use('/api/posts', postsRouter); 81 | }; 82 | -------------------------------------------------------------------------------- /client/app/components/autocomplete-input.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | store: Ember.inject.service(), 5 | 6 | foundItems: null, 7 | newItem: null, 8 | 9 | addItem(value) { 10 | if (typeof value === 'string') { 11 | value = this.get('store').createRecord('tag', { 12 | name: value 13 | }); 14 | } 15 | if (this.get('items')) { 16 | this.get('items').addObject(value); 17 | } else if (this.get('item')) { 18 | this.set('item', value); 19 | } 20 | this.set('foundItems', []); 21 | this.set('newItem', null); 22 | }, 23 | 24 | removeItem(value) { 25 | if (this.get('items')) { 26 | this.get('items').removeObject(value); 27 | } else if (this.get('item')) { 28 | this.set('item', undefined); 29 | } 30 | }, 31 | 32 | findItem(value) { 33 | let { store, model, key } = this.getProperties('store', 'model', 'key'); 34 | 35 | let query = {}; 36 | query[key] = { '$regex': value }; 37 | 38 | store.query(model, query).then((items) => { 39 | this.set('foundItems', items); 40 | }); 41 | }, 42 | 43 | actions: { 44 | remove(value) { 45 | this.removeItem(value); 46 | }, 47 | 48 | add(value) { 49 | this.addItem(value); 50 | }, 51 | 52 | keyUp(value, event) { 53 | let key = event.keyCode; 54 | switch (key) { 55 | // This is comma 56 | case 188: 57 | value = value.slice(0, -1); 58 | this.addItem(value); 59 | return; 60 | 61 | // Enter key 62 | case 13: 63 | this.addItem(value); 64 | return; 65 | 66 | default: 67 | if (value) { 68 | this.findItem(value); 69 | } else { 70 | // No need of querying and displaying old values 71 | // if value is empty 72 | this.set('foundItems', []); 73 | } 74 | return; 75 | 76 | } 77 | } 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /server/models/post.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var marked = require('marked'); 3 | var Tag = require('./tag'); 4 | var Schema = mongoose.Schema; 5 | 6 | var PostSchema = new Schema({ 7 | title: String, 8 | body: String, 9 | markdown: String, 10 | description: String, 11 | slug: String, 12 | dateCreated: Date, 13 | isPublished: Boolean, 14 | tags: [{ type: Schema.Types.ObjectId, ref: 'Tag' }], 15 | project: { type: Schema.Types.ObjectId, ref: 'Project' } 16 | }); 17 | 18 | /* 19 | * Creates a slug 20 | */ 21 | function slugify(text) { 22 | 23 | return text 24 | .toString().toLowerCase() 25 | .replace(/\s+/g, '-') // Replace spaces with - 26 | .replace(/[^\w\-]+/g, '') // Remove all non-word chars 27 | .replace(/\-\-+/g, '-') // Replace multiple - with single - 28 | .replace(/^-+/, '') // Trim - from start of text 29 | .replace(/-+$/, ''); // Trim - from end of text 30 | } 31 | 32 | /* 33 | * Transliteration 34 | */ 35 | function translit(text) { 36 | var letters = { 37 | 'а': 'a', 38 | 'б': 'b', 39 | 'в': 'v', 40 | 'г': 'g', 41 | 'д': 'd', 42 | 'е': 'e', 43 | 'ё': 'e', 44 | 'ж': 'zh', 45 | 'з': 'z', 46 | 'и': 'i', 47 | 'й': 'j', 48 | 'к': 'k', 49 | 'л': 'l', 50 | 'м': 'm', 51 | 'н': 'n', 52 | 'о': 'o', 53 | 'п': 'p', 54 | 'р': 'r', 55 | 'с': 's', 56 | 'т': 't', 57 | 'у': 'u', 58 | 'ф': 'f', 59 | 'х': 'h', 60 | 'ц': 'c', 61 | 'ч': 'ch', 62 | 'ш': 'sh', 63 | 'щ': 'sh', 64 | 'ъ': '\'', 65 | 'ы': 'y', 66 | 'ь': '\'', 67 | 'э': 'e', 68 | 'ю': 'yu', 69 | 'я': 'ya' 70 | }; 71 | 72 | var string = ''; 73 | 74 | Array.prototype.forEach.call(text.toLowerCase(), function(char) { 75 | if (letters[char]) { 76 | string += letters[char]; 77 | } else { 78 | string += char; 79 | } 80 | }); 81 | 82 | return string; 83 | } 84 | 85 | PostSchema.pre('save', function(next) { 86 | if (!this.title || !this.markdown) { 87 | throw 'No valid post object is specified'; 88 | } 89 | this.slug = translit(this.title); 90 | this.slug = slugify(this.slug); 91 | this.body = marked(this.markdown); 92 | next(); 93 | }); 94 | 95 | module.exports = mongoose.model('Post', PostSchema); 96 | -------------------------------------------------------------------------------- /client/app/templates/components/post-edit-form.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#admin-toolbar}} 3 | {{#if canRestore}} 4 |
  • 5 | [F1] Restore 6 |
  • 7 | {{/if}} 8 |
  • 9 | [F2] Save 10 |
  • 11 |
  • 12 | {{#if isPreview}} 13 | [F3] Edit 14 | {{else}} 15 | [F3] Preview 16 | {{/if}} 17 |
  • 18 |
  • 19 | {{#if post.id}} 20 | {{link-to "[F10] Close" "blog-posts.post-read" post}} 21 | {{else}} 22 | {{link-to "[F10] Close" "blog-posts.posts-list"}} 23 | {{/if}} 24 |
  • 25 |
  • 26 | [F11] Fullscreen 27 |
  • 28 | {{/admin-toolbar}} 29 |

    30 | {{moment-from-now post.dateCreated interval=60000}} 31 |

    32 |
    33 |
    34 |

    35 | {{form-title}} 36 |

    37 |
    38 | 39 | {{input id="title" value=post.title placeholder="post title" type="text"}} 40 | 41 | {{input id="published" type="checkbox" name="isPublished" checked=post.isPublished}} 42 | 43 | {{autocomplete-input item=post.project key="name" model="project"}} 44 | 45 | {{autocomplete-input items=post.tags key="name" model="tag"}} 46 | 47 | {{textarea id="description" value=post.description placeholder="post description" type="text"}} 48 |
    49 |
    50 | 51 | {{image-uploader action="insertLink"}} 52 |
    53 |
    54 |
    55 |
    56 |
    57 |

    58 | Preview 59 |

    60 | {{#blog-post-body}} 61 | {{{preview}}} 62 | {{/blog-post-body}} 63 |
    64 |
    65 | -------------------------------------------------------------------------------- /client/app/templates/projects/list.hbs: -------------------------------------------------------------------------------- 1 | {{title "List"}} 2 |
    3 | {{#admin-toolbar}} 4 |
  • 5 | [F3] Sync 6 |
  • 7 |
  • 8 | {{link-to "[F4] New project" "projects.create"}} 9 |
  • 10 | {{/admin-toolbar}} 11 | 76 |
    77 | -------------------------------------------------------------------------------- /client/app/styles/_generic.scss: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | color: $accent; 4 | cursor: pointer; 5 | 6 | &:hover { 7 | text-decoration: underline; 8 | } 9 | } 10 | 11 | img { 12 | display: block; 13 | margin: 0 auto; 14 | vertical-align: middle; 15 | border: 0; 16 | max-width: 95%; 17 | } 18 | 19 | table { 20 | width: 100%; 21 | } 22 | 23 | p { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | form { 29 | input:not([type]), 30 | input[type="text"], 31 | input[type="password"], 32 | input[type="submit"], 33 | textarea { 34 | font-family: 'Roboto Mono', monospace; 35 | display: block; 36 | margin-bottom: 20px; 37 | width: 100%; 38 | border: 0; 39 | outline: 0; 40 | font-size: 16px; 41 | } 42 | 43 | input:not([type]), 44 | input[type="text"], 45 | input[type="password"], 46 | input[type="submit"] { 47 | border-bottom: 1px solid $grey-light; 48 | padding: 5px 0px; 49 | 50 | &[type="submit"] { 51 | color: white; 52 | font-weight: 700; 53 | background-color: $green; 54 | } 55 | } 56 | 57 | input[type="checkbox"] { 58 | width: 16px; 59 | height: 16px; 60 | margin-bottom: 20px; 61 | background: none; 62 | border: 1px solid $grey-light; 63 | } 64 | 65 | label { 66 | display: block; 67 | font-weight: 500; 68 | margin-bottom: 10px; 69 | &:after { 70 | content: ":" 71 | } 72 | } 73 | 74 | div.inline-input { 75 | margin: 20px 0; 76 | 77 | label { 78 | margin: 0px; 79 | position: relative; 80 | display: inline-block; 81 | vertical-align: middle; 82 | } 83 | 84 | input[type="checkbox"] { 85 | margin: 0px; 86 | display: inline-block; 87 | position: relative; 88 | vertical-align: middle; 89 | } 90 | } 91 | 92 | } 93 | 94 | dl { 95 | dt { 96 | font-weight: 500; 97 | &:after { 98 | content: ": " 99 | } 100 | } 101 | } 102 | 103 | // LISTS 104 | 105 | ol { 106 | padding-left: 75px; 107 | 108 | @include max-screen(768px) { 109 | padding-left: 20px; 110 | } 111 | } 112 | 113 | ul { 114 | list-style: none; 115 | 116 | @include max-screen(768px) { 117 | padding-left: 20px; 118 | } 119 | } 120 | 121 | // LISTS END 122 | 123 | // HEADERS 124 | 125 | h1, h2, h3, h4 { 126 | padding: 0; 127 | margin: 0; 128 | margin-bottom: 20px; 129 | 130 | @include max-screen(768px) { 131 | font-size: 180%; 132 | margin-bottom: 10px; 133 | } 134 | } 135 | 136 | h1 { 137 | a { 138 | color: $grey-dark; 139 | } 140 | } 141 | 142 | // HEADERS END 143 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | var config = require('./config/config'); 2 | var express = require('express'); 3 | var path = require('path'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | var mongoose = require('mongoose'); 8 | var passport = require('passport'); 9 | var compression = require('compression'); 10 | 11 | var app = express(); 12 | 13 | // setting up mongodb connection 14 | mongoose.connect(config.database); 15 | 16 | app.use(compression({ 17 | level: 9 18 | })); 19 | 20 | // view engine setup 21 | app.set('views', path.join(__dirname, 'views')); 22 | app.set('view engine', 'jade'); 23 | app.set('secret', config.secret); 24 | app.use(require('cookie-parser')()); 25 | app.use(require('express-session')({ secret: config.secret, resave: false, saveUninitialized: false })); 26 | 27 | // Initialize Passport and restore authentication state, if any, from the 28 | // session. 29 | app.use(passport.initialize()); 30 | app.use(passport.session()); 31 | 32 | var allowCrossDomain = function(req, res, next) { 33 | res.header('Access-Control-Allow-Origin', 'https://kuzzmi.com'); 34 | if (app.get('env') === 'development') { 35 | res.header('Access-Control-Allow-Origin', '*'); 36 | } 37 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); 38 | res.header('Access-Control-Allow-Headers', [ 39 | 'Content-Type', 40 | 'Accept', 41 | 'Authorization', 42 | 'Content-Length', 43 | 'X-Requested-With' 44 | ].join(', ')); 45 | 46 | // intercept OPTIONS method 47 | if ('OPTIONS' === req.method) { 48 | res.sendStatus(200); 49 | } else { 50 | next(); 51 | } 52 | }; 53 | 54 | app.use(allowCrossDomain); 55 | 56 | app.use(logger('dev')); 57 | app.use(bodyParser.json()); 58 | app.use(bodyParser.urlencoded({ 59 | extended: false 60 | })); 61 | app.use(cookieParser()); 62 | app.use(express.static(path.join(__dirname, 'public'))); 63 | 64 | // Routes 65 | if (app.get('env') === 'development') { 66 | app.use('/api', require('./api')); 67 | } else { 68 | app.use('/', require('./api')); 69 | } 70 | 71 | // catch 404 and forward to error handler 72 | app.use(function(req, res, next) { 73 | var err = new Error('Not Found'); 74 | err.status = 404; 75 | next(err); 76 | }); 77 | 78 | // error handlers 79 | 80 | // development error handler 81 | // will print stacktrace 82 | if (app.get('env') === 'development') { 83 | app.use(function(err, req, res, next) { 84 | res.status(err.status || 500); 85 | res.json({ 86 | 'error': { 87 | message: err.message, 88 | error: err 89 | } 90 | }); 91 | }); 92 | } 93 | 94 | // production error handler 95 | // no stacktraces leaked to user 96 | app.use(function(err, req, res, next) { 97 | res.status(err.status || 500); 98 | res.json({ 99 | 'error': { 100 | message: err.message, 101 | error: {} 102 | } 103 | }); 104 | }); 105 | 106 | module.exports = app; 107 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | var crypto = require('crypto'); 4 | 5 | var UserSchema = new Schema({ 6 | username: String, 7 | email: { type: String, lowercase: true }, 8 | role: { 9 | type: String, 10 | default: 'user' 11 | }, 12 | hashedPassword: String, 13 | provider: String, 14 | salt: String 15 | }); 16 | 17 | /** 18 | * Virtuals 19 | */ 20 | UserSchema 21 | .virtual('password') 22 | .set(function(password) { 23 | this._password = password; 24 | this.salt = this.makeSalt(); 25 | this.hashedPassword = this.encryptPassword(password); 26 | }) 27 | .get(function() { 28 | return this._password; 29 | }); 30 | 31 | // Public profile information 32 | UserSchema 33 | .virtual('profile') 34 | .get(function() { 35 | return { 36 | 'username': this.username, 37 | 'role': this.role 38 | }; 39 | }); 40 | 41 | // Non-sensitive info we'll be putting in the token 42 | UserSchema 43 | .virtual('token') 44 | .get(function() { 45 | return { 46 | '_id': this._id, 47 | 'role': this.role 48 | }; 49 | }); 50 | 51 | /** 52 | * Validations 53 | */ 54 | 55 | // Validate empty email 56 | UserSchema 57 | .path('email') 58 | .validate(function(email) { 59 | return email.length; 60 | }, 'Email cannot be blank'); 61 | 62 | // Validate empty password 63 | UserSchema 64 | .path('hashedPassword') 65 | .validate(function(hashedPassword) { 66 | return hashedPassword.length; 67 | }, 'Password cannot be blank'); 68 | 69 | // Validate email is not taken 70 | UserSchema 71 | .path('email') 72 | .validate(function(value, respond) { 73 | var self = this; 74 | this.constructor.findOne({email: value}, function(err, user) { 75 | if(err) throw err; 76 | if(user) { 77 | if(self.id === user.id) return respond(true); 78 | return respond(false); 79 | } 80 | respond(true); 81 | }); 82 | }, 'The specified email address is already in use.'); 83 | 84 | var validatePresenceOf = function(value) { 85 | return value && value.length; 86 | }; 87 | 88 | /** 89 | * Pre-save hook 90 | */ 91 | UserSchema 92 | .pre('save', function(next) { 93 | if (!this.isNew) return next(); 94 | 95 | if (!validatePresenceOf(this.hashedPassword)) 96 | next(new Error('Invalid password')); 97 | else 98 | next(); 99 | }); 100 | 101 | /** 102 | * Methods 103 | */ 104 | UserSchema.methods = { 105 | /** 106 | * Authenticate - check if the passwords are the same 107 | * 108 | * @param {String} plainText 109 | * @return {Boolean} 110 | * @api public 111 | */ 112 | authenticate: function(plainText) { 113 | return this.encryptPassword(plainText) === this.hashedPassword; 114 | }, 115 | 116 | /** 117 | * Make salt 118 | * 119 | * @return {String} 120 | * @api public 121 | */ 122 | makeSalt: function() { 123 | return crypto.randomBytes(16).toString('base64'); 124 | }, 125 | 126 | /** 127 | * Encrypt password 128 | * 129 | * @param {String} password 130 | * @return {String} 131 | * @api public 132 | */ 133 | encryptPassword: function(password) { 134 | if (!password || !this.salt) return ''; 135 | var salt = new Buffer(this.salt, 'base64'); 136 | return crypto.pbkdf2Sync(password, salt, 10000, 64).toString('base64'); 137 | } 138 | }; 139 | 140 | module.exports = mongoose.model('User', UserSchema); 141 | -------------------------------------------------------------------------------- /server/api/users/route.js: -------------------------------------------------------------------------------- 1 | var User = require('../../models/user'); 2 | var passport = require('passport'); 3 | var config = require('../../config/config'); 4 | var jwt = require('jsonwebtoken'); 5 | 6 | var validationError = function(res, err) { 7 | return res.status(422).json(err); 8 | }; 9 | 10 | /** 11 | * Get list of users 12 | * restriction: 'admin' 13 | */ 14 | exports.index = function(req, res) { 15 | User.find({}, '-salt -hashedPassword', function(err, users) { 16 | if (err) return res.status(500).send(err); 17 | res.status(200).json(users); 18 | }); 19 | }; 20 | 21 | /** 22 | * Creates a new user 23 | */ 24 | exports.create = function(req, res, next) { 25 | var newUser = new User(req.body); 26 | newUser.provider = 'local'; 27 | User.find({}, '-salt -hashedPassword', function(err, users) { 28 | if (err) return res.status(500).send(err); 29 | if (users && users.length) { 30 | newUser.role = 'user'; 31 | } else { 32 | newUser.role = 'admin'; 33 | } 34 | newUser.save(function(err, user) { 35 | if (err) return validationError(res, err); 36 | var token = jwt.sign({ 37 | _id: user._id 38 | }, config.secret, { 39 | expiresIn: 60 * 60 * 5 40 | }); 41 | res.json({ 42 | token: token 43 | }); 44 | }); 45 | }); 46 | }; 47 | 48 | /** 49 | * Get a single user 50 | */ 51 | exports.show = function(req, res, next) { 52 | var userId = req.params.id; 53 | 54 | User.findById(userId, function(err, user) { 55 | if (err) return next(err); 56 | if (!user) return res.status(401).send('Unauthorized'); 57 | res.json(user.profile); 58 | }); 59 | }; 60 | 61 | /** 62 | * Deletes a user 63 | * restriction: 'admin' 64 | */ 65 | exports.destroy = function(req, res) { 66 | User.findByIdAndRemove(req.params.id, function(err, user) { 67 | if (err) return res.status(500).send(err); 68 | return res.status(204).send('No Content'); 69 | }); 70 | }; 71 | 72 | /** 73 | * Change a users password 74 | */ 75 | exports.changePassword = function(req, res, next) { 76 | var userId = req.user._id; 77 | var oldPass = String(req.body.oldPassword); 78 | var newPass = String(req.body.newPassword); 79 | 80 | User.findById(userId, function(err, user) { 81 | if (user.authenticate(oldPass)) { 82 | user.password = newPass; 83 | user.save(function(err) { 84 | if (err) return validationError(res, err); 85 | res.status(200).send('OK'); 86 | }); 87 | } else { 88 | res.status(403).send('Forbidden'); 89 | } 90 | }); 91 | }; 92 | 93 | /** 94 | * Get my info 95 | */ 96 | exports.me = function(req, res, next) { 97 | var userId = req.user._id; 98 | User.findOne({ 99 | _id: userId 100 | }, '-salt -hashedPassword', function(err, user) { // don't ever give out the password or salt 101 | if (err) return next(err); 102 | if (!user) return res.status(401).send('Unauthorized'); 103 | res.json(user); 104 | }); 105 | }; 106 | 107 | /** 108 | * Get my info 109 | */ 110 | exports.setup = function(req, res, next) { 111 | User.find({}, '-salt -hashedPassword', function(err, users) { 112 | if (err) return res.status(500).send(err); 113 | if (users.length !== 0) { 114 | var error = new Error('Users route has been already setup'); 115 | return res.status(500).send(error); 116 | } 117 | res.status(200).json(users); 118 | }); 119 | }; 120 | 121 | /** 122 | * Authentication callback 123 | */ 124 | exports.authCallback = function(req, res, next) { 125 | res.redirect('/'); 126 | }; 127 | -------------------------------------------------------------------------------- /client/config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | var ENV = { 5 | modulePrefix: 'kuzzmi-blog', 6 | environment: environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | disqus: { 10 | shortname: 'kuzzmi' 11 | }, 12 | EmberENV: { 13 | FEATURES: { 14 | // Here you can enable experimental features on an ember canary build 15 | // e.g. 'with-controller': true 16 | } 17 | }, 18 | 19 | APP: { 20 | // Here you can pass flags/options to your application instance 21 | // when it is created 22 | }, 23 | 24 | API: { 25 | namespace: 'api' 26 | // Here will be stored API specific settings 27 | }, 28 | 29 | contentSecurityPolicy: { 30 | 'script-src': "'self' 'unsafe-inline' kuzzmi.disqus.com referrer.disqus.com www.google-analytics.com static.addtoany.com", 31 | 'style-src': "'self' 'unsafe-inline' use.typekit.net fonts.googleapis.com a.disquscdn.com static.addtoany.com", 32 | 'frame-src': "'self' 'unsafe-inline' disqus.com static.addtoany.com www.livecoding.tv", 33 | 'child-src': "'self' 'unsafe-inline' disqus.com", 34 | 'font-src': "'self' 'unsafe-inline' fonts.gstatic.com", 35 | 'img-src': "'self' 'unsafe-inline' www.gravatar.com a.disquscdn.com referrer.disqus.com data:", 36 | 'connect-src': "'self' http://localhost:3000" 37 | }, 38 | 39 | 'ember-simple-auth': { 40 | // Here we can configure ember-simple-auth 41 | authenticationRoute: 'auth.login', 42 | routeAfterAuthentication: 'blog-posts', 43 | routeIfAlreadyAuthenticated: 'blog-posts' 44 | }, 45 | 46 | author: { 47 | nickname: 'kuzzmi', 48 | email: 'igor@kuzzmi.com', 49 | facebook: 'https://www.facebook.com/ikuzmenko', 50 | name: 'Igor', 51 | lastName: 'Kuzmenko' 52 | }, 53 | 54 | webFontConfig: { 55 | google: { 56 | families: ['Roboto Mono:400,500,700'] 57 | } 58 | }, 59 | 60 | pace: { 61 | theme: 'minimal' 62 | } 63 | }; 64 | 65 | if (environment === 'development') { 66 | ENV.API.host = 'http://localhost:3000'; 67 | ENV.API.uploadPath = '/uploads'; 68 | // ENV.APP.LOG_RESOLVER = true; 69 | ENV.APP.LOG_ACTIVE_GENERATION = true; 70 | ENV.APP.LOG_TRANSITIONS = true; 71 | ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 72 | ENV.APP.LOG_VIEW_LOOKUPS = true; 73 | } 74 | 75 | if (environment === 'test') { 76 | // Testem prefers this... 77 | ENV.rootURL = '/'; 78 | ENV.locationType = 'none'; 79 | 80 | // keep test console output quieter 81 | ENV.APP.LOG_ACTIVE_GENERATION = false; 82 | ENV.APP.LOG_VIEW_LOOKUPS = false; 83 | 84 | ENV.APP.rootElement = '#ember-testing'; 85 | } 86 | 87 | if (environment === 'production') { 88 | ENV.API.host = 'https://kuzzmi.com'; 89 | ENV.API.uploadPath = '/uploads'; 90 | ENV.googleAnalytics = { 91 | webPropertyId: 'UA-51775404-4' 92 | }; 93 | ENV.contentSecurityPolicy = { 94 | 'script-src': "'self' 'unsafe-inline' www.google-analytics.com", 95 | 'style-src': "'self' 'unsafe-inline' use.typekit.net fonts.googleapis.com", 96 | 'connect-src': "'self' www.google-analytics.com", 97 | 'font-src': "'self' 'unsafe-inline' fonts.gstatic.com", 98 | 'img-src': "'self' 'unsafe-inline' www.gravatar.com" 99 | }; 100 | } 101 | 102 | return ENV; 103 | }; 104 | -------------------------------------------------------------------------------- /server/api/projects/route.js: -------------------------------------------------------------------------------- 1 | var githubAPI = require('node-github'); 2 | var mongoose = require('mongoose'); 3 | var extend = require('extend'); 4 | var Project = require('../../models/project'); 5 | 6 | module.exports.add = function(req, res) { 7 | var project = new Project(req.body.project); 8 | project.save(function(err) { 9 | if (err) { 10 | res.status(500).send(err); 11 | } 12 | res.json({ 13 | project: project 14 | }); 15 | }); 16 | }; 17 | 18 | module.exports.getAll = function(req, res) { 19 | if (!req.user) { 20 | req.query.isPublished = true; 21 | } else if (req.query.isPublished) { 22 | delete req.query.isPublished; 23 | } 24 | 25 | Project.find(req.query) 26 | .sort({ 27 | dateUpdated: 'desc', 28 | githubID: 'asc' 29 | }) 30 | .exec(function(err, projects) { 31 | if (err) { 32 | res.status(500).send(err); 33 | } 34 | res.json({ 35 | projects: projects 36 | }); 37 | }); 38 | }; 39 | 40 | module.exports.update = function(req, res) { 41 | Project.findOne({ 42 | _id: req.params.id 43 | }, function(err, project) { 44 | if (err) { 45 | res.status(500).send(err); 46 | } 47 | 48 | project.update(req.body.project, function(err, project) { 49 | if (err) { 50 | res.status(500).send(err); 51 | } 52 | res.json({ 53 | project: project 54 | }); 55 | }); 56 | }); 57 | }; 58 | 59 | module.exports.getOne = function(req, res, id) { 60 | Project.findOne({ 61 | _id: req.params.id 62 | }, function(err, project) { 63 | if (err) { 64 | res.status(500).send(err); 65 | } 66 | res.json({ 67 | project: project 68 | }); 69 | }); 70 | }; 71 | 72 | module.exports.sync = function(req, res) { 73 | var github = new githubAPI({ 74 | version: '3.0.0' 75 | }); 76 | 77 | github.repos.getFromUser({ 78 | user: 'kuzzmi' 79 | }, function(err, data) { 80 | if (err && res) { 81 | res.status(500).send(err); 82 | } 83 | 84 | data.forEach(function(_project) { 85 | Project.findOne({ 86 | githubID: _project.id 87 | }, function(err, project) { 88 | if (err && res) { 89 | res.status(500).send(err); 90 | } 91 | 92 | if (!project) { 93 | project = new Project({ 94 | githubID: _project.id, 95 | name: _project.name, 96 | url: _project.html_url, 97 | description: _project.description, 98 | dateCreated: _project.created_at, 99 | dateUpdated: _project.pushed_at, 100 | isOwner: !_project.fork, 101 | stars: _project.stargazers_count, 102 | isPublished: false, 103 | posts: [] 104 | }); 105 | 106 | project.save(function(err) { 107 | if (err && res) { 108 | res.status(500).send(err); 109 | } 110 | }); 111 | } else { 112 | project.name = _project.name; 113 | project.description = _project.description; 114 | project.url = _project.html_url; 115 | project.dateUpdated = _project.pushed_at; 116 | project.stars = _project.stargazers_count; 117 | project.save(function(err) { 118 | if (err && res) { 119 | res.status(500).send(err); 120 | } 121 | }); 122 | } 123 | }); 124 | }); 125 | 126 | if (res) { 127 | res.json(data); 128 | } 129 | }); 130 | }; 131 | -------------------------------------------------------------------------------- /server/api/posts/route.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mongoose = require('mongoose'); 4 | var extend = require('extend'); 5 | var Post = require('../../models/post'); 6 | var Tag = require('../../models/tag'); 7 | var fs = require('fs'); 8 | var config = require('../../config/config'); 9 | 10 | module.exports.add = function(req, res) { 11 | var post = new Post(req.body.post); 12 | 13 | post.save(function(err, post) { 14 | if (err) { 15 | res.send(err); 16 | } 17 | Post.populate(post, 'tags project', function() { 18 | res.json({ 19 | post: post 20 | }); 21 | }); 22 | }); 23 | }; 24 | 25 | module.exports.getAll = function(req, res) { 26 | var limits = { 27 | skip: ( req.query.page - 1 ) * req.query.size, 28 | limit: parseInt(req.query.size, 10) 29 | } 30 | 31 | delete req.query.page; 32 | delete req.query.size; 33 | 34 | if (!req.user) { 35 | req.query.isPublished = true; 36 | } else if (req.query.isPublished) { 37 | delete req.query.isPublished; 38 | } 39 | 40 | Post.find(req.query) 41 | .populate('tags') 42 | .limit(limits.limit) 43 | .skip(limits.skip) 44 | .sort('-dateCreated') 45 | // .populate('project') 46 | .exec(function(err, posts) { 47 | if (err) { 48 | res.send(err); 49 | } 50 | res.json({ 51 | posts: posts 52 | }); 53 | }); 54 | }; 55 | 56 | module.exports.getCount = function(req, res) { 57 | if (!req.user) { 58 | req.query.isPublished = true; 59 | } else if (req.query.isPublished) { 60 | delete req.query.isPublished; 61 | } 62 | 63 | Post.find(req.query) 64 | .populate('tags') 65 | .sort('-dateCreated') 66 | // .populate('project') 67 | .count(function(err, count) { 68 | if (err) { 69 | res.send(err); 70 | } 71 | res.json({ 72 | count: count 73 | }); 74 | }); 75 | }; 76 | 77 | module.exports.getOne = function(req, res) { 78 | var query = { 79 | slug: req.params.slug, 80 | isPublished: true 81 | }; 82 | 83 | if (req.user) { 84 | delete query.isPublished; 85 | } 86 | 87 | Post.findOne(query) 88 | .populate('tags') 89 | // .populate('project') 90 | .exec(function(err, post) { 91 | if (err) { 92 | res.send(err); 93 | } 94 | res.json({ 95 | post: post 96 | }); 97 | }); 98 | }; 99 | 100 | module.exports.update = function(req, res) { 101 | var id = req.params.id; 102 | 103 | Post.findById(id, function(err, post) { 104 | if (err) { 105 | res.send(err); 106 | } 107 | 108 | var newPost = req.body.post; 109 | post.markdown = newPost.markdown; 110 | post.tags = newPost.tags; 111 | post.title = newPost.title; 112 | post.description = newPost.description; 113 | post.project = newPost.project; 114 | post.isPublished = newPost.isPublished; 115 | 116 | post.save(function(err, post) { 117 | if (err) { 118 | res.send(err); 119 | } 120 | Post.populate(post, 'tags', function() { 121 | res.json({ 122 | post: post 123 | }); 124 | }); 125 | }); 126 | }); 127 | }; 128 | 129 | module.exports.delete = function(req, res) { 130 | var id = req.params.id; 131 | 132 | Post.findOne({ 133 | _id: id 134 | }, function(err, post) { 135 | if (err) { 136 | res.send(err); 137 | } 138 | post.remove(function(err) { 139 | if (err) { 140 | res.send(err); 141 | } 142 | res.json({}); 143 | }); 144 | }); 145 | }; 146 | 147 | module.exports.upload = function(req, res) { 148 | res.send(req.file.filename); 149 | }; 150 | 151 | module.exports.deleteUploaded = function(req, res) { 152 | fs.unlink(config.uploadPath + req.params.id, (err) => { 153 | if (err) { 154 | res.status(500).send(err); 155 | return; 156 | } 157 | 158 | res.status(200).json({ status: 'OK' }); 159 | }); 160 | }; 161 | -------------------------------------------------------------------------------- /server/auth/service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mongoose = require('mongoose'); 4 | var passport = require('passport'); 5 | var config = require('../config/config'); 6 | var jwt = require('jsonwebtoken'); 7 | var expressJwt = require('express-jwt'); 8 | var compose = require('composable-middleware'); 9 | var User = require('../models/user'); 10 | var validateJwt = expressJwt({ 11 | secret: config.secret 12 | }); 13 | var validateJwtNotStrict = expressJwt({ 14 | secret: config.secret, 15 | credentialsRequired: false 16 | }); 17 | 18 | function validate(notStrict) { 19 | return compose() 20 | // Validate jwt 21 | .use(function(req, res, next) { 22 | // allow access_token to be passed through query parameter as well 23 | if (req.query && req.query.hasOwnProperty('access_token')) { 24 | req.headers.authorization = 'Bearer ' + req.query.access_token; 25 | } 26 | if (notStrict) { 27 | validateJwtNotStrict(req, res, next); 28 | } else { 29 | validateJwt(req, res, next); 30 | } 31 | }); 32 | } 33 | 34 | /** 35 | * Attaches the user object to the request if authenticated 36 | * Otherwise returns 403 37 | */ 38 | function isAuthenticated() { 39 | return compose() 40 | .use(validate()) 41 | // Attach user to request 42 | .use(function(req, res, next) { 43 | User.findById(req.user._id, function(err, user) { 44 | if (err) return next(err); 45 | if (!user) return res.status(401).send('Unauthorized'); 46 | 47 | req.user = user; 48 | next(); 49 | }); 50 | }); 51 | } 52 | 53 | function attachUser() { 54 | return compose() 55 | .use(validate(true)) 56 | // Attach user to request 57 | .use(function(req, res, next) { 58 | if (!req.user) { 59 | return next(); 60 | } 61 | User.findById(req.user._id, function(err, user) { 62 | if (err) return next(err); 63 | if (user) 64 | req.user = user; 65 | next(); 66 | }); 67 | }); 68 | } 69 | 70 | /** 71 | * Checks if the user role meets the minimum requirements of the route 72 | * if not will return 73 | */ 74 | function hasRoleNotStrict(roleRequired) { 75 | if (!roleRequired) throw new Error('Required role needs to be set'); 76 | 77 | return compose() 78 | .use(attachUser()) 79 | .use(function meetsRequirements(req, res, next) { 80 | if (req.user) { 81 | if (config.userRoles.indexOf(req.user.role) >= config.userRoles.indexOf(roleRequired)) { 82 | next(); 83 | } else { 84 | delete req.user; 85 | next(); 86 | } 87 | } else { 88 | next(); 89 | } 90 | }); 91 | } 92 | 93 | /** 94 | * Checks if the user role meets the minimum requirements of the route 95 | */ 96 | function hasRole(roleRequired) { 97 | if (!roleRequired) throw new Error('Required role needs to be set'); 98 | 99 | return compose() 100 | .use(isAuthenticated()) 101 | .use(function meetsRequirements(req, res, next) { 102 | if (config.userRoles.indexOf(req.user.role) >= config.userRoles.indexOf(roleRequired)) { 103 | next(); 104 | } else { 105 | res.status(403).send('Forbidden'); 106 | } 107 | }); 108 | } 109 | 110 | /** 111 | * Returns a jwt token signed by the app secret 112 | */ 113 | function signToken(id, role) { 114 | return jwt.sign({ 115 | _id: id, 116 | role: role 117 | }, config.secret, { 118 | expiresIn: config.tokenExpiration 119 | }); 120 | } 121 | 122 | /** 123 | * Set token cookie directly for oAuth strategies 124 | */ 125 | function setTokenCookie(req, res) { 126 | if (!req.user) return res.status(404).json({ 127 | message: 'Something went wrong, please try again.' 128 | }); 129 | var token = signToken(req.user._id, req.user.role); 130 | res.cookie('token', JSON.stringify(token)); 131 | res.redirect('/'); 132 | } 133 | 134 | exports.isAuthenticated = isAuthenticated; 135 | exports.hasRole = hasRole; 136 | exports.hasRoleNotStrict = hasRoleNotStrict; 137 | exports.signToken = signToken; 138 | exports.setTokenCookie = setTokenCookie; 139 | exports.attachUser = attachUser; 140 | -------------------------------------------------------------------------------- /client/app/components/post-edit-form.js: -------------------------------------------------------------------------------- 1 | /* globals CodeMirror */ 2 | /* globals hljs */ 3 | 4 | import Ember from 'ember'; 5 | import config from '../config/environment'; 6 | import { storageFor } from 'ember-local-storage'; 7 | 8 | const cm = CodeMirror; 9 | 10 | export default Ember.Component.extend({ 11 | store: Ember.inject.service(), 12 | api: Ember.inject.service(), 13 | backup: storageFor('postBackup'), 14 | editor: null, 15 | preview: '', 16 | isPreview: false, 17 | images: [], 18 | 19 | /* events */ 20 | didInsertElement() { 21 | let self = this; 22 | this._super(...arguments); 23 | let myTextarea = this.$('#editor-body')[0]; 24 | let editor = cm(myTextarea, { 25 | value: this.get('post.markdown') || '', 26 | mode: 'markdown', 27 | lineWrapping: true, 28 | keyMap: 'vim' 29 | }); 30 | 31 | editor.on('change', function(editor) { 32 | let body = editor.getValue(); 33 | self.set('post.markdown', body); 34 | }); 35 | 36 | this.set('editor', editor); 37 | 38 | this.$('input:first').focus(); 39 | }, 40 | 41 | init() { 42 | this._super(...arguments); 43 | }, 44 | 45 | canRestore: Ember.computed('backup.markdown', function() { 46 | return this.get('backup.markdown'); 47 | }), 48 | 49 | /* actions */ 50 | actions: { 51 | restore() { 52 | let editor = this.get('editor'); 53 | if (this.get('backup.markdown')) { 54 | editor.doc.setValue(this.get('backup.markdown')); 55 | this.get('backup').clear(); 56 | } 57 | }, 58 | save(post) { 59 | if (!post.get('dateCreated')) { 60 | post.set('dateCreated', new Date()); 61 | } 62 | 63 | let images = this.get('images'); 64 | 65 | for (var i = 0, l = images.length; i < l; i++) { 66 | var v = images[i]; 67 | 68 | if (this.get('post.markdown').indexOf(v.url) === -1) { 69 | this.get('api').call(true, 'posts/upload/' + v.filename, { 70 | method: 'DELETE' 71 | }); 72 | } 73 | } 74 | 75 | this.sendAction('save', post); 76 | }, 77 | 78 | edit() { 79 | Ember.$('.form').toggleClass('hidden'); 80 | Ember.$('.preview').toggleClass('hidden'); 81 | this.set('isPreview', false); 82 | }, 83 | 84 | preview(post) { 85 | let url = [config.API.host, config.API.namespace, 'posts', 'preview'].join('/'); 86 | let self = this; 87 | Ember.$.ajax({ 88 | type: 'POST', 89 | url: url, 90 | data: { 91 | body: post.get('markdown') 92 | } 93 | }).success((data) => { 94 | self.set('preview', data.html); 95 | self.set('isPreview', true); 96 | Ember.$('.form').toggleClass('hidden'); 97 | Ember.$('.preview').toggleClass('hidden'); 98 | setTimeout(function() { 99 | Ember.$('pre code').each((i, block) => { 100 | hljs.highlightBlock(block); 101 | }); 102 | }, 0); 103 | }); 104 | }, 105 | 106 | insertLink(image) { 107 | let editor = this.get('editor'); 108 | 109 | let val = editor.doc.getValue().split('\n'); 110 | let cur = editor.doc.getCursor(); 111 | let img = '![](' + image.url + ')'; 112 | let scroll = editor.getScrollInfo(); 113 | 114 | val.splice(cur.line + 1, 0, '', img, '', ''); 115 | 116 | editor.doc.setValue(val.join('\n')); 117 | 118 | editor.doc.setCursor(cur.line + 4); 119 | editor.scrollTo(0, scroll.top); 120 | editor.focus(); 121 | 122 | this.get('images').addObject(image); 123 | }, 124 | 125 | fullscreen() { 126 | Ember.$('.not-editor').toggleClass('hidden'); 127 | Ember.$('h1').toggleClass('hidden'); 128 | Ember.$('.current-time').toggleClass('hidden'); 129 | Ember.$('.toolbar').toggleClass('hidden'); 130 | Ember.$('nav').toggleClass('hidden'); 131 | Ember.$('.editor').toggleClass('fullscreen'); 132 | this.get('editor').refresh(); 133 | } 134 | } 135 | }); 136 | -------------------------------------------------------------------------------- /client/app/routes/blog-posts/post-read.js: -------------------------------------------------------------------------------- 1 | /* globals a2a */ 2 | /* globals a2a_config */ 3 | import Ember from 'ember'; 4 | import config from '../../config/environment'; 5 | 6 | export default Ember.Route.extend({ 7 | controllerName: 'blog-posts', 8 | 9 | model(post) { 10 | let { slug } = post; 11 | return this.store.query('post', { slug }).then((posts) => { 12 | if (posts.content.length) { 13 | return posts.get('firstObject'); 14 | } else { 15 | this.transitionTo('404'); 16 | } 17 | }); 18 | }, 19 | 20 | afterModel: function(model, transition) { 21 | this.setHeadTags(model); 22 | 23 | transition.then(() => { 24 | a2a_config.linkname = model.get('title'); 25 | a2a_config.linkurl = window.location.origin + this.get('router.url'); 26 | 27 | setTimeout(function() { 28 | a2a.init('page'); 29 | }, 10); 30 | setTimeout(function() { 31 | a2a.init('page'); 32 | }, 10); 33 | }); 34 | }, 35 | 36 | setHeadTags: function(model) { 37 | const author = config['author'] || {}; 38 | const fullname = [ author.name, author.lastName ].join(' '); 39 | let tags = {}; 40 | 41 | tags = [{ 42 | type: 'meta', 43 | tagId: 'meta-twitter-card-tag', 44 | attrs: { 45 | property: 'twitter:card', 46 | content: 'summary' 47 | } 48 | }, { 49 | type: 'meta', 50 | tagId: 'meta-twitter-title-tag', 51 | attrs: { 52 | property: 'twitter:title', 53 | content: model.get('title') 54 | } 55 | }, { 56 | type: 'meta', 57 | tagId: 'meta-twitter-site-tag', 58 | attrs: { 59 | property: 'twitter:site', 60 | content: '@' + author.nickname 61 | } 62 | }, { 63 | type: 'meta', 64 | tagId: 'meta-twitter-creator-tag', 65 | attrs: { 66 | property: 'twitter:creator', 67 | content: '@' + author.nickname 68 | } 69 | }, { 70 | type: 'meta', 71 | tagId: 'meta-twitter-description-tag', 72 | attrs: { 73 | property: 'twitter:description', 74 | content: model.get('description') 75 | } 76 | }, { 77 | type: 'meta', 78 | tagId: 'meta-og-type-tag', 79 | attrs: { 80 | property: 'og:type', 81 | content: 'article' 82 | } 83 | }, { 84 | type: 'meta', 85 | tagId: 'meta-og-section-tag', 86 | attrs: { 87 | property: 'article:section', 88 | content: 'Software Development' 89 | } 90 | }, { 91 | type: 'meta', 92 | tagId: 'meta-og-author-tag', 93 | attrs: { 94 | property: 'article:author', 95 | content: author.facebook 96 | } 97 | }, { 98 | type: 'meta', 99 | tagId: 'meta-og-title-tag', 100 | attrs: { 101 | property: 'og:title', 102 | content: model.get('title') 103 | } 104 | }, { 105 | type: 'meta', 106 | tagId: 'meta-og-description-tag', 107 | attrs: { 108 | property: 'og:description', 109 | content: model.get('description') 110 | } 111 | }, { 112 | type: 'meta', 113 | tagId: 'meta-og-url-tag', 114 | attrs: { 115 | property: 'og:url', 116 | content: window.location.href 117 | } 118 | }, { 119 | type: 'meta', 120 | tagId: 'meta-title-tag', 121 | attrs: { 122 | name: 'title', 123 | content: model.get('title') 124 | } 125 | }, { 126 | type: 'meta', 127 | tagId: 'meta-description-tag', 128 | attrs: { 129 | name: 'description', 130 | content: model.get('description') 131 | } 132 | }, { 133 | type: 'meta', 134 | tagId: 'meta-author-tag', 135 | attrs: { 136 | name: 'author', 137 | content: fullname 138 | } 139 | }]; 140 | 141 | this.set('headTags', tags); 142 | }, 143 | 144 | actions: { 145 | delete(post) { 146 | this.store.findRecord('post', post.id).then((_post) => { 147 | _post.destroyRecord().then(() => { 148 | this.transitionTo('blog-posts'); 149 | }); 150 | }); 151 | } 152 | } 153 | }); 154 | -------------------------------------------------------------------------------- /client/app/styles/_media-queries.scss: -------------------------------------------------------------------------------- 1 | // Author: Rafal Bromirski 2 | // www: http://rafalbromirski.com/ 3 | // github: http://github.com/paranoida/sass-mediaqueries 4 | // 5 | // Licensed under a MIT License 6 | // 7 | // Version: 8 | // 1.6.1 9 | 10 | // --- generator --------------------------------------------------------------- 11 | 12 | @mixin mq($args...) { 13 | $media-type: 'only screen'; 14 | $media-type-key: 'media-type'; 15 | $args: keywords($args); 16 | $expr: ''; 17 | 18 | @if map-has-key($args, $media-type-key) { 19 | $media-type: map-get($args, $media-type-key); 20 | $args: map-remove($args, $media-type-key); 21 | } 22 | 23 | @each $key, $value in $args { 24 | @if $value { 25 | $expr: "#{$expr} and (#{$key}: #{$value})"; 26 | } 27 | } 28 | 29 | @media #{$media-type} #{$expr} { 30 | @content; 31 | } 32 | } 33 | 34 | // --- screen ------------------------------------------------------------------ 35 | 36 | @mixin screen($min, $max, $orientation: false) { 37 | @include mq($min-width: $min, $max-width: $max, $orientation: $orientation) { 38 | @content; 39 | } 40 | } 41 | 42 | @mixin max-screen($max) { 43 | @include mq($max-width: $max) { 44 | @content; 45 | } 46 | } 47 | 48 | @mixin min-screen($min) { 49 | @include mq($min-width: $min) { 50 | @content; 51 | } 52 | } 53 | 54 | @mixin screen-height($min, $max, $orientation: false) { 55 | @include mq($min-height: $min, $max-height: $max, $orientation: $orientation) { 56 | @content; 57 | } 58 | } 59 | 60 | @mixin max-screen-height($max) { 61 | @include mq($max-height: $max) { 62 | @content; 63 | } 64 | } 65 | 66 | @mixin min-screen-height($min) { 67 | @include mq($min-height: $min) { 68 | @content; 69 | } 70 | } 71 | 72 | // --- hdpi -------------------------------------------------------------------- 73 | 74 | @mixin hdpi($ratio: 1.3) { 75 | @media only screen and (-webkit-min-device-pixel-ratio: $ratio), 76 | only screen and (min-resolution: #{round($ratio*96)}dpi) { 77 | @content; 78 | } 79 | } 80 | 81 | // --- hdtv -------------------------------------------------------------------- 82 | 83 | @mixin hdtv($standard: '1080') { 84 | $min-width: false; 85 | $min-height: false; 86 | 87 | $standards: ('720p', 1280px, 720px) 88 | ('1080', 1920px, 1080px) 89 | ('2K', 2048px, 1080px) 90 | ('4K', 4096px, 2160px); 91 | 92 | @each $s in $standards { 93 | @if $standard == nth($s, 1) { 94 | $min-width: nth($s, 2); 95 | $min-height: nth($s, 3); 96 | } 97 | } 98 | 99 | @include mq( 100 | $min-device-width: $min-width, 101 | $min-device-height: $min-height, 102 | $min-width: $min-width, 103 | $min-height: $min-height 104 | ) { 105 | @content; 106 | } 107 | } 108 | 109 | // --- iphone4 ----------------------------------------------------------------- 110 | 111 | @mixin iphone4($orientation: false) { 112 | $min: 320px; 113 | $max: 480px; 114 | $pixel-ratio: 2; 115 | $aspect-ratio: '2/3'; 116 | 117 | @include mq( 118 | $min-device-width: $min, 119 | $max-device-width: $max, 120 | $orientation: $orientation, 121 | $device-aspect-ratio: $aspect-ratio, 122 | $-webkit-device-pixel-ratio: $pixel-ratio 123 | ) { 124 | @content; 125 | } 126 | } 127 | 128 | // --- iphone5 ----------------------------------------------------------------- 129 | 130 | @mixin iphone5($orientation: false) { 131 | $min: 320px; 132 | $max: 568px; 133 | $pixel-ratio: 2; 134 | $aspect-ratio: '40/71'; 135 | 136 | @include mq( 137 | $min-device-width: $min, 138 | $max-device-width: $max, 139 | $orientation: $orientation, 140 | $device-aspect-ratio: $aspect-ratio, 141 | $-webkit-device-pixel-ratio: $pixel-ratio 142 | ) { 143 | @content; 144 | } 145 | } 146 | 147 | // --- iphone6 ----------------------------------------------------------------- 148 | 149 | @mixin iphone6($orientation: false) { 150 | $min: 375px; 151 | $max: 667px; 152 | $pixel-ratio: 2; 153 | 154 | @include mq( 155 | $min-device-width: $min, 156 | $max-device-width: $max, 157 | $orientation: $orientation, 158 | $-webkit-device-pixel-ratio: $pixel-ratio 159 | ) { 160 | @content; 161 | } 162 | } 163 | 164 | // --- iphone6 plus ------------------------------------------------------------ 165 | 166 | @mixin iphone6-plus($orientation: false) { 167 | $min: 414px; 168 | $max: 736px; 169 | $pixel-ratio: 3; 170 | 171 | @include mq( 172 | $min-device-width: $min, 173 | $max-device-width: $max, 174 | $orientation: $orientation, 175 | $-webkit-device-pixel-ratio: $pixel-ratio 176 | ) { 177 | @content; 178 | } 179 | } 180 | 181 | // --- ipad (all) -------------------------------------------------------------- 182 | 183 | @mixin ipad($orientation: false) { 184 | $min: 768px; 185 | $max: 1024px; 186 | 187 | @include mq( 188 | $min-device-width: $min, 189 | $max-device-width: $max, 190 | $orientation: $orientation 191 | ) { 192 | @content; 193 | } 194 | } 195 | 196 | // --- ipad-retina ------------------------------------------------------------- 197 | 198 | @mixin ipad-retina($orientation: false) { 199 | $min: 768px; 200 | $max: 1024px; 201 | $pixel-ratio: 2; 202 | 203 | @include mq( 204 | $min-device-width: $min, 205 | $max-device-width: $max, 206 | $orientation: $orientation, 207 | $-webkit-device-pixel-ratio: $pixel-ratio 208 | ) { 209 | @content; 210 | } 211 | } 212 | 213 | // --- orientation ------------------------------------------------------------- 214 | 215 | @mixin landscape() { 216 | @include mq($orientation: landscape) { 217 | @content; 218 | } 219 | } 220 | 221 | @mixin portrait() { 222 | @include mq($orientation: portrait) { 223 | @content; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /client/app/styles/components/_nav-bar.scss: -------------------------------------------------------------------------------- 1 | @keyframes blink { 2 | 0% { 3 | opacity: 1; 4 | } 5 | 49% { 6 | opacity: 1; 7 | } 8 | 50% { 9 | opacity: 0; 10 | } 11 | 100% { 12 | opacity: 0; 13 | } 14 | } 15 | 16 | @keyframes live-indicator { 17 | from { 18 | opacity: 0.1; 19 | } 20 | to { 21 | opacity: 1; 22 | } 23 | } 24 | 25 | nav { 26 | margin: 80px 40px; 27 | position: relative; 28 | box-sizing: content-box; 29 | height: $font-size-navbar + 10px; 30 | 31 | @include max-screen(768px) { 32 | padding: 20px 0px; 33 | } 34 | 35 | .blog-title { 36 | // opacity: 0.1; 37 | height: 100%; 38 | position: relative; 39 | display: inline-block; 40 | text-transform: lowercase; 41 | 42 | @include max-screen(768px) { 43 | width: 100%; 44 | padding: 0 20px; 45 | } 46 | 47 | .nav-toggle { 48 | display: none; 49 | border: none; 50 | background: transparent; 51 | float: right; 52 | height: 34px; 53 | outline: 0; 54 | transform: rotate(0deg); 55 | transition: transform .25s cubic-bezier(.35,0,.25,1); 56 | 57 | span { 58 | display: block; 59 | height: 1px; 60 | margin: 3px 0; 61 | width: 15px; 62 | border-bottom: 2px solid $grey; 63 | } 64 | 65 | &.active { 66 | transform: rotate(90deg); 67 | span { 68 | border-bottom: 2px solid $accent; 69 | } 70 | } 71 | 72 | @include max-screen(768px) { 73 | display: inline-block; 74 | } 75 | } 76 | 77 | a { 78 | float: left; 79 | top: -8px; 80 | color: $accent; 81 | font-size: $font-size-navbar; 82 | font-weight: 500; 83 | 84 | @include max-screen(768px) { 85 | position: relative; 86 | top: -2px; 87 | } 88 | 89 | .title { 90 | display: inline-block; 91 | } 92 | 93 | .animated-dot { 94 | color: black; 95 | animation-name: blink; 96 | animation-duration: 0.5s; 97 | animation-direction: alternate; 98 | animation-iteration-count: infinite; 99 | } 100 | 101 | .rec-sign { 102 | width: 16px; 103 | height: 16px; 104 | background-color: red; 105 | border-radius: 100%; 106 | display: inline-block; 107 | top: -14px; 108 | left: 4px; 109 | position: relative; 110 | animation-name: live-indicator; 111 | animation-duration: 1s; 112 | animation-direction: alternate; 113 | animation-iteration-count: infinite; 114 | } 115 | } 116 | 117 | color: white; 118 | 119 | } 120 | 121 | .navbar { 122 | position: relative; 123 | margin-left: 20px; 124 | display: inline-block; 125 | height: 100%; 126 | 127 | @include max-screen(768px) { 128 | transform: translate(-100%, 0); 129 | transition: transform .25s cubic-bezier(.35,0,.25,1); 130 | display: block; 131 | position: fixed; 132 | left: -20px; 133 | right: 70px; 134 | top: 0; 135 | bottom: 0; 136 | background-color: rgb(230, 230, 230); 137 | &.show { 138 | transform: translate(0, 0); 139 | } 140 | z-index: 100; 141 | } 142 | 143 | .active-indicator { 144 | background: black; 145 | height: 100%; 146 | top: 0; 147 | position: absolute; 148 | display: inline-block; 149 | z-index: -1; 150 | transition: left .25s cubic-bezier(.35,0,.25,1), right .125s cubic-bezier(.35,0,.25,1); 151 | @include max-screen(768px) { 152 | display: none; 153 | } 154 | } 155 | 156 | ul { 157 | margin: 0px; 158 | padding: 0px; 159 | height: 100%; 160 | list-style: none; 161 | display: inline-block; 162 | overflow: hidden; 163 | z-index: 10; 164 | 165 | li { 166 | display: inline-block; 167 | text-transform: lowercase; 168 | font-weight: 500; 169 | font-size: $font-size-navbar; 170 | padding: 0 10px; 171 | 172 | @include max-screen(768px) { 173 | display: block; 174 | padding-top: 20px; 175 | padding-left: 20px; 176 | } 177 | 178 | a { 179 | transition: color .25s cubic-bezier(.35,0,.25,1), background-color .125s cubic-bezier(.35,0,.25,1); 180 | padding: 0 10px; 181 | color: $grey-dark; 182 | &.active { 183 | color: white; 184 | @include max-screen(768px) { 185 | background-color: black; 186 | } 187 | } 188 | 189 | &.rss-icon { 190 | color: $accent; 191 | font-size: 33px; 192 | position: absolute; 193 | top: -4px; 194 | margin-left: 210px; 195 | @include max-screen(768px) { 196 | position: initial; 197 | margin-left: 0px; 198 | } 199 | } 200 | } 201 | 202 | } 203 | } 204 | } 205 | 206 | } 207 | --------------------------------------------------------------------------------