├── app ├── views │ ├── .gitkeep │ ├── users │ │ └── add.js │ └── packages │ │ └── list.js ├── components │ ├── .gitkeep │ ├── code-snippet.js │ ├── focus-input.js │ ├── checkbox-group.js │ ├── drop-down.js │ ├── package-icon.js │ ├── checkbox-group-item.js │ └── auto-complete-text-input.js ├── helpers │ ├── .gitkeep │ ├── is-equal.js │ └── format-date.js ├── models │ ├── .gitkeep │ ├── search-results.js │ ├── user.js │ ├── package.js │ └── semantic-version.js ├── routes │ ├── .gitkeep │ ├── packages │ │ ├── list.js │ │ ├── advanced-search.js │ │ ├── view.js │ │ └── search.js │ ├── admin.js │ ├── symbols.js │ ├── login.js │ ├── users │ │ ├── edit.js │ │ ├── list.js │ │ └── add.js │ ├── not-found.js │ ├── profile.js │ ├── error.js │ └── application.js ├── styles │ ├── .gitkeep │ ├── _kl-table.scss │ ├── _kl-code.scss │ ├── _kl-text.scss │ ├── _kl-icon.scss │ ├── _kl-button.scss │ ├── _kl-progress.scss │ ├── app.scss │ ├── _variables.scss │ ├── _kl-dropdown.scss │ └── _kl-white-space.scss ├── templates │ ├── .gitkeep │ ├── components │ │ ├── .gitkeep │ │ ├── checkbox-group.hbs │ │ ├── checkbox-group-item.hbs │ │ ├── code-snippet.hbs │ │ ├── drop-down.hbs │ │ ├── auto-complete-text-input.hbs │ │ └── search-link-list.hbs │ ├── error.hbs │ ├── denied.hbs │ ├── index.hbs │ ├── users │ │ ├── list.hbs │ │ └── edit.hbs │ ├── login.hbs │ ├── packages │ │ ├── advanced-search.hbs │ │ ├── search.hbs │ │ └── view.hbs │ ├── profile.hbs │ ├── admin.hbs │ ├── symbols.hbs │ └── application.hbs ├── controllers │ ├── .gitkeep │ ├── index.js │ ├── packages │ │ ├── list.js │ │ ├── advanced-search.js │ │ ├── search.js │ │ └── view.js │ ├── users │ │ ├── list.js │ │ ├── add.js │ │ └── edit.js │ ├── admin.js │ ├── error.js │ ├── login.js │ ├── profile.js │ └── application.js ├── progress-indicator.js ├── services │ ├── signalR.js │ ├── hubs.js │ ├── package-indexer.js │ └── rest-client.js ├── mixins │ ├── base-controller.js │ ├── progress-indicator-route.js │ ├── user-permission-observer.js │ ├── pagination-support.js │ └── authorized-route.js ├── application-exception.js ├── app.js ├── util │ └── describe-promise.js ├── index.html ├── router.js ├── adapters │ ├── user.js │ └── package.js ├── stores │ └── main.js ├── initializers │ └── dependencies.js └── session.js ├── vendor └── .gitkeep ├── tests ├── unit │ └── .gitkeep ├── test-helper.js ├── helpers │ ├── destroy-app.js │ ├── resolver.js │ ├── start-app.js │ └── module-for-acceptance.js ├── .jshintrc └── index.html ├── COPYRIGHT.txt ├── .watchmanconfig ├── public ├── robots.txt └── assets │ └── package-default-icon-50x50.png ├── .bowerrc ├── .gitmodules ├── src ├── Klondike.SelfHost.Tests │ ├── packages.config │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── CommandLineSettingsTests.cs │ ├── SelfHostVirtualPathUtilityTests.cs │ ├── Klondike.SelfHost.Tests.csproj │ └── app.config ├── Klondike.SelfHost │ ├── App.Dist.config │ ├── Program.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── README.md │ ├── CommandLineSettings.cs │ ├── SelfHostVirtualPathUtility.cs │ ├── KlondikeService.cs │ ├── SelfHostSettings.cs │ ├── SelfHostStartup.cs │ └── packages.config └── Klondike.WebHost │ ├── Web.Release.config │ ├── Properties │ └── AssemblyInfo.cs │ ├── MetaController.cs │ ├── IVirtualPathUtility.cs │ ├── AppBuilderExtensions.cs │ ├── Startup.cs │ ├── KlondikeHtmlMicrodataFormatter.cs │ ├── packages.config │ └── Settings.config ├── .idea └── .idea.Klondike │ └── .idea │ ├── encodings.xml │ ├── vcs.xml │ ├── indexLayout.xml │ ├── .gitignore │ └── aws.xml ├── .nuget └── packages.config ├── testem.json ├── NuGet.config ├── .gitattributes ├── .gitignore ├── .jshintrc ├── Ciao.props ├── .editorconfig ├── bower.json ├── appveyor.yml ├── package.json ├── config └── environment.js ├── ember-cli-build.js ├── Klondike.sln ├── server └── index.js ├── Ciao.targets ├── README.md └── Ciao.proj /app/views/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/styles/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2013-2015 The Motley Fool 2 | -------------------------------------------------------------------------------- /app/progress-indicator.js: -------------------------------------------------------------------------------- 1 | export default window.NProgress; 2 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp","build","dist"] 3 | } 4 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /app/services/signalR.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.$.signalR; 4 | -------------------------------------------------------------------------------- /app/styles/_kl-table.scss: -------------------------------------------------------------------------------- 1 | .kl-table--sortable { 2 | th:hover { 3 | cursor: pointer; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dist"] 2 | path = dist 3 | url = git@github.com:themotleyfool/Klondike-Release.git 4 | -------------------------------------------------------------------------------- /app/templates/components/checkbox-group.hbs: -------------------------------------------------------------------------------- 1 | {{#each content as |item|}} 2 | {{checkbox-group-item content=item}} 3 | {{/each}} 4 | -------------------------------------------------------------------------------- /app/routes/packages/list.js: -------------------------------------------------------------------------------- 1 | import PackagesSearchRoute from './search'; 2 | 3 | export default PackagesSearchRoute.extend({ 4 | }); 5 | -------------------------------------------------------------------------------- /app/views/users/add.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | export default Ember.Component.extend({ 3 | layoutName: 'users/edit' 4 | }); 5 | -------------------------------------------------------------------------------- /app/components/code-snippet.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: 'div' 5 | }); 6 | -------------------------------------------------------------------------------- /app/templates/components/checkbox-group-item.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/components/code-snippet.hbs: -------------------------------------------------------------------------------- 1 |
{{#if prompt}}{{prompt}}{{/if}}{{content}}
2 | -------------------------------------------------------------------------------- /app/views/packages/list.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | export default Ember.Component.extend({ 3 | layoutName: 'packages/search' 4 | }); 5 | -------------------------------------------------------------------------------- /public/assets/package-default-icon-50x50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriseldredge/Klondike/HEAD/public/assets/package-default-icon-50x50.png -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | 6 | setResolver(resolver); 7 | -------------------------------------------------------------------------------- /tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default function destroyApp(application) { 4 | Ember.run(application, 'destroy'); 5 | } 6 | -------------------------------------------------------------------------------- /app/helpers/is-equal.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | export default Ember.Helper.helper(function([leftSide, rightSide]) { 4 | return leftSide === rightSide; 5 | }); 6 | -------------------------------------------------------------------------------- /app/routes/admin.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | export default Ember.Route.extend({ 3 | model: function () { 4 | return this.get('indexer'); 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost.Tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/styles/_kl-code.scss: -------------------------------------------------------------------------------- 1 | $pre-padding: 0.5rem; 2 | 3 | .code-snippet { 4 | padding: $pre-padding; 5 | border-left: 4px solid $blue; 6 | border-radius: 0; 7 | white-space: pre-wrap; 8 | } 9 | -------------------------------------------------------------------------------- /app/routes/symbols.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | export default Ember.Route.extend({ 3 | model: function () { 4 | return this.get('restClient').ajax('symbols.getSettings'); 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /app/components/focus-input.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.TextField.extend({ 4 | becomeFocused: function() { 5 | this.$().focus(); 6 | }.on('didInsertElement') 7 | }); 8 | -------------------------------------------------------------------------------- /.idea/.idea.Klondike/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/templates/components/drop-down.hbs: -------------------------------------------------------------------------------- 1 | {{#each content key="@index" as |item|}} 2 | 5 | {{/each}} 6 | -------------------------------------------------------------------------------- /app/templates/error.hbs: -------------------------------------------------------------------------------- 1 |
2 |

{{title}}

3 | 4 | {{#if statusCode}} 5 |
{{statusCode}}
6 | {{/if}} 7 | 8 |

{{message}}

9 |
10 | -------------------------------------------------------------------------------- /.idea/.idea.Klondike/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/styles/_kl-text.scss: -------------------------------------------------------------------------------- 1 | .kl-span--low-importance { 2 | color: $text-low-importance-color; 3 | font-size: $text-low-importance-font-size; 4 | } 5 | 6 | .kl-summary--package-heading { 7 | font-size: $text-h2-font-size; 8 | } 9 | -------------------------------------------------------------------------------- /.nuget/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/helpers/format-date.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Helper.helper(function([date], options) { 4 | var format = options.format || 'dddd, MMMM Do YYYY, HH:mm:ss A Z'; 5 | return window.moment(date).format(format); 6 | }); 7 | -------------------------------------------------------------------------------- /app/templates/denied.hbs: -------------------------------------------------------------------------------- 1 |
2 |

You can't!

3 | 4 |

Sorry, but you do not have permission to view the requested content.

5 | 6 |

Please check your permissions or sign in with another account.

7 |
8 | -------------------------------------------------------------------------------- /.idea/.idea.Klondike/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/models/search-results.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Object.extend({ 4 | query: '', 5 | page: 0, 6 | pageSize: 0, 7 | offset: 0, 8 | includePrerelease: false, 9 | totalHits: 0, 10 | hits: [], 11 | }); 12 | -------------------------------------------------------------------------------- /app/routes/login.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | export default Ember.Route.extend({ 3 | setupController: function(controller, model) { 4 | controller.set('username', ''); 5 | controller.set('password', ''); 6 | this._super(controller, model); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /app/templates/components/auto-complete-text-input.hbs: -------------------------------------------------------------------------------- 1 | {{focus-input type="text" value=value class="advanced-search m0 field col-12 rounded-left"}} 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/styles/_kl-icon.scss: -------------------------------------------------------------------------------- 1 | .kl-icon { 2 | width: 1em; 3 | height: 1em; 4 | position: relative; 5 | top: 0.125em; 6 | } 7 | 8 | // Icon sizes 9 | .kl-icon--20 { 10 | width: 20px; 11 | height: 20px; 12 | } 13 | 14 | .kl-icon--50 { 15 | width: 50px; 16 | height: 50px; 17 | } 18 | -------------------------------------------------------------------------------- /app/styles/_kl-button.scss: -------------------------------------------------------------------------------- 1 | .kl-button { 2 | 3 | &.is-disabled { 4 | opacity: 0.5; 5 | pointer-events: none; 6 | } 7 | } 8 | 9 | .kl-button--link { 10 | color: $link-color; 11 | 12 | &:hover { 13 | background-image: none; 14 | text-decoration: underline; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/mixins/base-controller.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Mixin.create({ 4 | packageSourceUri: Ember.computed('restClient.packageSourceUri', { 5 | get: function() { 6 | return this.get('restClient.packageSourceUri') || 'loading...'; 7 | } 8 | }) 9 | }); 10 | -------------------------------------------------------------------------------- /app/application-exception.js: -------------------------------------------------------------------------------- 1 | var applicationException = function (message) { 2 | this.message = message; 3 | if (Error.captureStackTrace) { 4 | Error.captureStackTrace(this); 5 | } else { 6 | this.stack = new Error().stack; 7 | } 8 | }; 9 | 10 | export default applicationException; 11 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember/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 | -------------------------------------------------------------------------------- /app/routes/users/edit.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ProgressIndicatorRoute from 'klondike/mixins/progress-indicator-route'; 3 | import AuthorizedRoute from 'klondike/mixins/authorized-route'; 4 | 5 | export default Ember.Route.extend(ProgressIndicatorRoute, AuthorizedRoute, { 6 | authorizedApiName: 'users.post' 7 | }); 8 | -------------------------------------------------------------------------------- /app/components/checkbox-group.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: 'ul', 5 | classNames: ['ember-checkbox-group'], 6 | name: null, 7 | selection: null, 8 | content: null, 9 | checkboxLabelPath: 'content', 10 | checkboxValuePath: 'content' 11 | }); 12 | -------------------------------------------------------------------------------- /app/routes/not-found.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ApplicationException from 'klondike/application-exception'; 3 | 4 | export default Ember.Route.extend({ 5 | afterModel: function() { 6 | var ex = new ApplicationException("Invalid Route"); 7 | ex.request = { status: 404 }; 8 | throw ex; 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /app/styles/_kl-progress.scss: -------------------------------------------------------------------------------- 1 | // Custom overrides for NProgress indidcators 2 | 3 | #nprogress { 4 | 5 | .bar { 6 | background: white; 7 | } 8 | 9 | .peg { 10 | box-shadow: 0 0 10px white, 0 0 5px white; 11 | } 12 | 13 | .spinner-icon { 14 | border-top-color: white; 15 | border-left-color: white; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/controllers/index.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import BaseControllerMixin from 'klondike/mixins/base-controller'; 3 | 4 | export default Ember.Controller.extend(BaseControllerMixin, { 5 | packageSourceCommand: function() { 6 | return 'nuget sources add -name Klondike -source ' + this.get('packageSourceUri'); 7 | }.property('packageSourceUri') 8 | }); 9 | -------------------------------------------------------------------------------- /.idea/.idea.Klondike/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /contentModel.xml 6 | /modules.xml 7 | /.idea.Klondike.iml 8 | /projectSettingsUpdater.xml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | # Datasource local storage ignored files 12 | /dataSources/ 13 | /dataSources.local.xml 14 | -------------------------------------------------------------------------------- /app/models/user.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Object.extend({ 4 | idBinding: 'username', 5 | username: '', 6 | key: '', 7 | roles: Ember.A(), 8 | allRoles: Ember.A([ 9 | { name: 'PackageManager', label: 'Package Manager' }, 10 | { name: 'AccountAdministrator', label: 'Account Administrator' } 11 | ]) 12 | }); 13 | -------------------------------------------------------------------------------- /app/routes/users/list.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ProgressIndicatorRoute from 'klondike/mixins/progress-indicator-route'; 3 | 4 | export default Ember.Route.extend(ProgressIndicatorRoute, { 5 | beforeModel: function() { 6 | return this.get('session'); 7 | }, 8 | model: function () { 9 | return this.get('store').list('user'); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost/App.Dist.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/routes/packages/advanced-search.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model: function () { 5 | return this.get('packages').getAvailableSearchFields(); 6 | }, 7 | 8 | actions: { 9 | invalidSearch: function(error) { 10 | this.get('controller').send('invalidSearch', error); 11 | return false; 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /app/controllers/packages/list.js: -------------------------------------------------------------------------------- 1 | import PackagesSearchController from './search'; 2 | 3 | export default PackagesSearchController.extend({ 4 | sortBy: 'id', 5 | 6 | init: function() 7 | { 8 | // remove "relevance" from sort options 9 | var sortByColumns = this.get('sortByColumns'); 10 | this.set('sortByColumns', sortByColumns.slice(1)); 11 | 12 | this._super(); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /app/routes/users/add.js: -------------------------------------------------------------------------------- 1 | import UsersEditRoute from './edit'; 2 | 3 | export default UsersEditRoute.extend({ 4 | authorizedApiName: 'users.put', 5 | 6 | model: function() { 7 | return this.get('store').createModel('user'); 8 | }, 9 | 10 | setupController: function(controller, model) { 11 | controller.reset(); 12 | this._super(controller, model); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/styles/app.scss: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | @import 'variables'; 3 | 4 | // Vendor 5 | @import 'bower_components/font-awesome/scss/font-awesome'; 6 | @import 'bower_components/basscss-sass/basscss'; 7 | 8 | // Modules 9 | @import 'kl-icon'; 10 | @import 'kl-button'; 11 | @import 'kl-code'; 12 | @import 'kl-dropdown'; 13 | @import 'kl-progress'; 14 | @import 'kl-table'; 15 | @import 'kl-text'; 16 | 17 | // Additional Utilities 18 | @import 'kl-white-space'; 19 | -------------------------------------------------------------------------------- /app/controllers/users/list.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import BaseControllerMixin from 'klondike/mixins/base-controller'; 3 | import UserPermissionObserver from 'klondike/mixins/user-permission-observer'; 4 | 5 | export default Ember.Controller.extend(BaseControllerMixin, UserPermissionObserver, { 6 | canEdit: false, 7 | 8 | init: function() { 9 | this._super(); 10 | this.observeUserPermission('canEdit', 'users.put'); 11 | }, 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /app/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $fa-font-path: "font-awesome/fonts"; 2 | $footer-height: 3em; 3 | $auto-complete-select-color: #88f; 4 | $auto-complete-input-width: 50em; 5 | 6 | 7 | // Customize BassCSS variables 8 | $silver: #eee; 9 | $blue: #0074d9; 10 | 11 | $link-color: $blue; 12 | 13 | $button-font-weight: normal; 14 | 15 | $pre-background-color: $silver; 16 | 17 | $text-low-importance-color: #aaa; 18 | $text-low-importance-font-size: smaller; 19 | 20 | $text-h2-font-size: 1.3em; 21 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from 'ember/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 | -------------------------------------------------------------------------------- /app/templates/components/search-link-list.hbs: -------------------------------------------------------------------------------- 1 | {{#if attrs.items}} 2 | {{#if attrs.header}} 3 | 4 | {{/if}} 5 | 12 | {{/if}} 13 | -------------------------------------------------------------------------------- /app/util/describe-promise.js: -------------------------------------------------------------------------------- 1 | export default function(type, methodName, methodParams) { 2 | methodParams = methodParams || []; 3 | if (!methodParams.join) { 4 | methodParams = Array.prototype.slice.call(methodParams); 5 | } 6 | 7 | var name = type.toString(); 8 | 9 | if (!methodName) { 10 | return name; 11 | } 12 | 13 | name += '.' + methodName; 14 | name += '(' + methodParams.map(JSON.stringify).join(', ') + ')'; 15 | 16 | return name; 17 | } 18 | -------------------------------------------------------------------------------- /app/mixins/progress-indicator-route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ProgressIndicator from 'klondike/progress-indicator'; 3 | 4 | export default Ember.Mixin.create({ 5 | start: ProgressIndicator.start, 6 | done: ProgressIndicator.done, 7 | 8 | actions: { 9 | loading: function() { 10 | this.start(); 11 | }, 12 | 13 | error: function() { 14 | this.done(); 15 | }, 16 | 17 | didTransition: function() { 18 | this.done(); 19 | } 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /app/models/package.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import SemanticVersion from './semantic-version'; 3 | 4 | export default Ember.Object.extend({ 5 | id: '', 6 | version: '', 7 | versionHistory: [], 8 | 9 | displayTitle: function() { 10 | return this.get('title') || this.get('id'); 11 | }.property('title', 'id'), 12 | 13 | semanticVersion: function() { 14 | return SemanticVersion.create({version: this.get('version')}); 15 | }.property('version') 16 | }); 17 | -------------------------------------------------------------------------------- /app/routes/profile.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import AuthorizedRoute from 'klondike/mixins/authorized-route'; 3 | import describePromise from 'klondike/util/describe-promise'; 4 | 5 | export default Ember.Route.extend(AuthorizedRoute, { 6 | authorizedApiName: 'users.getAuthenticationInfo', 7 | model: function () { 8 | var self = this; 9 | return this.get('session').then(function() { 10 | return self.get('session').get('user'); 11 | }, null, describePromise(this, 'model')); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /app/routes/error.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ProgressIndicator from 'klondike/progress-indicator'; 3 | 4 | export default Ember.Route.extend({ 5 | activate: function() { 6 | ProgressIndicator.done(); 7 | }, 8 | setupController: function(controller, model) { 9 | this._super(controller, model); 10 | model = model || {}; 11 | if (console && console.error) { 12 | console.error('Unhandled error:', model.message || model.errorThrown, model.stack); 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /app/routes/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | export default Ember.Route.extend({ 3 | beforeModel: function(transition) { 4 | this._saveTransition(transition); 5 | }, 6 | actions: { 7 | willTransition: function (transition) { 8 | this._saveTransition(transition); 9 | } 10 | }, 11 | _saveTransition: function (transition) { 12 | if (transition.targetName !== 'login') { 13 | this.controllerFor('login').set('previousTransition', transition); 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /.idea/.idea.Klondike/.idea/aws.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /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 | let attributes = Ember.merge({}, config.APP); 9 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; 10 | 11 | Ember.run(() => { 12 | application = Application.create(attributes); 13 | application.setupForTesting(); 14 | application.injectTestHelpers(); 15 | }); 16 | 17 | return application; 18 | } 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /app/components/drop-down.js: -------------------------------------------------------------------------------- 1 | import Ember from "ember"; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: 'select', 5 | content: [], 6 | selectedValue: null, 7 | 8 | didInsertElement: function() { 9 | const self = this; 10 | const $select = this.$(this.element); 11 | $select.on('change', function() { 12 | const selectedIndex = $select.prop('selectedIndex'); 13 | const content = self.get('content'); 14 | const selectedItem = content[selectedIndex]; 15 | 16 | self.set('selectedValue', selectedItem.value); 17 | }); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /app/templates/index.hbs: -------------------------------------------------------------------------------- 1 |
2 |

3 | Welcome to Klondike, the NuGet repository 4 | for caching public packages and storing your private packages privately. 5 |

6 | 7 |

Getting Started

8 | 9 |

To use this repository from Visual Studio, add a new Package Source using this URL:

10 | 11 | {{code-snippet content=packageSourceUri}} 12 | 13 |

Alternatively, you can add the package source from the command line as follows:

14 | 15 | {{code-snippet content=packageSourceCommand prompt='C:\> '}} 16 |
-------------------------------------------------------------------------------- /app/controllers/admin.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import BaseControllerMixin from 'klondike/mixins/base-controller'; 3 | 4 | export default Ember.Controller.extend(BaseControllerMixin, { 5 | canSynchronize: Ember.computed.oneWay('indexer.canSynchronize'), 6 | 7 | actions: { 8 | rebuild: function () { 9 | this.get('indexer').rebuild(); 10 | }, 11 | synchronize: function () { 12 | this.get('indexer').synchronize(); 13 | }, 14 | cancel: function () { 15 | this.get('indexer').cancel(); 16 | } 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /app/templates/users/list.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Accounts

3 | 4 | 15 | 16 | {{#if canEdit}} 17 | {{#link-to 'users.add' class="btn button-blue"}}Add Account{{/link-to}} 18 | {{else}} 19 |
You are not allowed to modify accounts.
20 | {{/if}} 21 |
22 | -------------------------------------------------------------------------------- /.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 | /.settings/ 14 | /connect.lock 15 | /coverage/* 16 | /libpeerconnection.log 17 | .DS_Store 18 | Thumbs.db 19 | .ember-cli 20 | 21 | # .net 22 | App_Data/ 23 | bin/ 24 | obj/ 25 | /build/ 26 | /Release/ 27 | /packages/ 28 | _NCrunch* 29 | *.ncrunch* 30 | *.user 31 | *.suo 32 | _ReSharper.* 33 | integration-tests/out.txt 34 | NuGet.exe 35 | Klondike.sln.ide/ 36 | npm-debug.log 37 | testem.log 38 | .vs/ 39 | -------------------------------------------------------------------------------- /tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import startApp from '../helpers/start-app'; 3 | import destroyApp from '../helpers/destroy-app'; 4 | 5 | export default function(name, options = {}) { 6 | module(name, { 7 | beforeEach() { 8 | this.application = startApp(); 9 | 10 | if (options.beforeEach) { 11 | options.beforeEach.apply(this, arguments); 12 | } 13 | }, 14 | 15 | afterEach() { 16 | destroyApp(this.application); 17 | 18 | if (options.afterEach) { 19 | options.afterEach.apply(this, arguments); 20 | } 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /app/controllers/users/add.js: -------------------------------------------------------------------------------- 1 | import UsersEditController from './edit'; 2 | 3 | export default UsersEditController.extend({ 4 | actionLabel: 'Create', 5 | canDelete: false, 6 | 7 | actions: { 8 | save: function () { 9 | this.set('errorMessage', ''); 10 | 11 | var user = { 12 | username: this.get('model.username'), 13 | key: this.get('model.key'), 14 | roles: this.get('model.roles') 15 | }; 16 | 17 | var promise = this.get('users').add(user); 18 | 19 | return this._wrapAjaxPromise(promise); 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /app/templates/login.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if isRedirected}} 3 |

4 | Please log in to continue. 5 |

6 | {{/if}} 7 | 8 | {{#if errorMessage}} 9 |
10 | {{errorMessage}} 11 |
12 | {{/if}} 13 | 17 | 21 | 22 | 23 |
24 | 25 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /Ciao.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Klondike.sln 5 | 0.0.1 6 | 7 | 8 | 9 | true 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.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 = 4 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 | -------------------------------------------------------------------------------- /app/components/package-icon.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | DefaultIconUrl: 'assets/package-default-icon-50x50.png', 5 | tagName: 'img', 6 | classNames: ['package-icon kl-icon--50 mr2'], 7 | attributeBindings: ['src', 'alt'], 8 | alt: 'package icon', 9 | 10 | url: null, 11 | 12 | src: function () { 13 | return this.get('url') || this.DefaultIconUrl; 14 | }.property('url'), 15 | 16 | didInsertElement: function () { 17 | var self = this; 18 | var img = this.$(); 19 | img.error(function () { 20 | img.unbind('error').attr('src', self.DefaultIconUrl); 21 | }); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /app/styles/_kl-dropdown.scss: -------------------------------------------------------------------------------- 1 | .kl-dropdown { 2 | background: white; 3 | box-shadow: 0 1px 2px 1px $darken-3; 4 | opacity: 0; 5 | visibility: hidden; 6 | height: 0; 7 | max-height: 20em; 8 | overflow-y: scroll; 9 | 10 | &.is-visible { 11 | opacity: 1; 12 | visibility: visible; 13 | height: auto; 14 | } 15 | 16 | & > li { 17 | padding: $space-1 $space-2; 18 | } 19 | 20 | &:hover > li.is-selected { 21 | background-color: inherit; 22 | color: inherit; 23 | } 24 | 25 | & > li.is-selected, 26 | & > li:hover, 27 | & > li.is-selected:hover { 28 | background: $blue; 29 | color: white; 30 | } 31 | 32 | & > li:hover { 33 | cursor: pointer; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "klondike", 3 | "dependencies": { 4 | "basscss-sass": "~2.0.0", 5 | "ember": "1.13.11", 6 | "ember-cli-shims": "0.0.6", 7 | "ember-cli-test-loader": "0.2.1", 8 | "ember-load-initializers": "0.1.7", 9 | "ember-qunit": "0.4.16", 10 | "ember-qunit-notifications": "0.1.0", 11 | "ember-resolver": "~0.1.20", 12 | "font-awesome": "~4.3.0", 13 | "geomicons-open": "~2.0.0", 14 | "jcaret": "#cada9e2c77", 15 | "jquery": "^2.1.3", 16 | "jquery-signalr": "https://github.com/chriseldredge/bower-jquery-signalr.git#2.1.1", 17 | "loader.js": "ember-cli/loader.js#3.4.0", 18 | "momentjs": "~2.9.0", 19 | "nprogress": "~0.1.2", 20 | "qunit": "~1.20.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Klondike.WebHost/Web.Release.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/controllers/error.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import BaseControllerMixin from 'klondike/mixins/base-controller'; 3 | 4 | export default Ember.Controller.extend(BaseControllerMixin, { 5 | title: 'Error', 6 | 7 | statusCode: function() { 8 | var model = this.get('model'); 9 | if (model && model.request) { 10 | return model.request.status || 0; 11 | } 12 | return 0; 13 | }.property('model'), 14 | 15 | message: function() { 16 | if (this.get('statusCode') === 404) { 17 | return 'The requested resource was not found.'; 18 | } 19 | 20 | return 'Looks like something went wrong. Check the javascript console for more details.'; 21 | }.property('model') 22 | }); 23 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Klondike - NuGet Package Repository 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 | 23 | {{content-for 'body-footer'}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from './config/environment'; 3 | 4 | const Router = Ember.Router.extend({ 5 | location: config.locationType 6 | }); 7 | 8 | Router.map(function() { 9 | this.route('index', { path: '/' }); 10 | this.route('login'); 11 | this.route('denied'); 12 | this.route('profile'); 13 | this.route('symbols'); 14 | this.route('admin'); 15 | this.resource('packages', function() { 16 | this.route('search', { path: '/search' }); 17 | this.route('advanced-search', { path: '/advanced-search' }); 18 | this.route('list', { path: '/list' }); 19 | this.route('view', { path: '/:id/:version' }); 20 | }); 21 | this.resource('users', function() { 22 | this.route('list', { path: '/' }); 23 | this.route('add', { path: '/add' }); 24 | this.route('edit', { path: '/edit/*user_id' }); 25 | }); 26 | this.route('not_found', { path: '/*not_found' }); 27 | }); 28 | 29 | export default Router; 30 | -------------------------------------------------------------------------------- /app/styles/_kl-white-space.scss: -------------------------------------------------------------------------------- 1 | // White space utility classes not included in BassCSS 2 | 3 | .p0 { padding: 0 } 4 | .pt0 { padding-top: 0 } 5 | .pr0 { padding-right: 0 } 6 | .pb0 { padding-bottom: 0 } 7 | .pl0 { padding-left: 0 } 8 | 9 | .p1 { padding: $space-1 } 10 | .pt1 { padding-top: $space-1 } 11 | .pr1 { padding-right: $space-1 } 12 | .pb1 { padding-bottom: $space-1 } 13 | .pl1 { padding-left: $space-1 } 14 | 15 | .p2 { padding: $space-2 } 16 | .pt2 { padding-top: $space-2 } 17 | .pr2 { padding-right: $space-2 } 18 | .pb2 { padding-bottom: $space-2 } 19 | .pl2 { padding-left: $space-2 } 20 | 21 | .p3 { padding: $space-3 } 22 | .pt3 { padding-top: $space-3 } 23 | .pr3 { padding-right: $space-3 } 24 | .pb3 { padding-bottom: $space-3 } 25 | .pl3 { padding-left: $space-3 } 26 | 27 | .p4 { padding: $space-4 } 28 | .pt4 { padding-top: $space-4 } 29 | .pr4 { padding-right: $space-4 } 30 | .pb4 { padding-bottom: $space-4 } 31 | .pl4 { padding-left: $space-4 } 32 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ServiceProcess; 3 | 4 | namespace Klondike.SelfHost 5 | { 6 | class Program 7 | { 8 | static Program() 9 | { 10 | // Make log4net use paths relative to application base. 11 | Environment.CurrentDirectory = AppDomain.CurrentDomain.SetupInformation.ApplicationBase; 12 | } 13 | 14 | static void Main(string[] args) 15 | { 16 | var settings = new SelfHostSettings(CommandLineSettings.Parse(args)); 17 | 18 | var service = new KlondikeService(settings); 19 | 20 | if (settings.Interactive || Environment.UserInteractive) 21 | { 22 | Console.WriteLine("Running interactively"); 23 | service.RunInteractivley(); 24 | } 25 | else 26 | { 27 | Console.WriteLine("Running as service"); 28 | ServiceBase.Run(service); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Klondike.WebHost/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("Klondike")] 8 | [assembly: AssemblyDescription("NuGet Package Server powered by NuGet.Lucene")] 9 | [assembly: AssemblyCompany("The Motley Fool, LLC")] 10 | [assembly: AssemblyProduct("Klondike")] 11 | [assembly: AssemblyCopyright("Copyright © The Motley Fool, LLC 2013")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | // Setting ComVisible to false makes the types in this assembly not visible 15 | // to COM components. If you need to access a type in this assembly from 16 | // COM, set the ComVisible attribute to true on that type. 17 | [assembly: ComVisible(false)] 18 | 19 | // The following GUID is for the ID of the typelib if this project is exposed to COM 20 | [assembly: Guid("4b191ae9-777d-4916-986c-1fe4ffdc1cab")] 21 | -------------------------------------------------------------------------------- /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 | "esnext": true, 51 | "unused": true 52 | } 53 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("Klondike.SelfHost")] 8 | [assembly: AssemblyDescription("Self-Hosted NuGet Package Server powered by NuGet.Lucene")] 9 | [assembly: AssemblyCompany("The Motley Fool, LLC")] 10 | [assembly: AssemblyProduct("Klondike")] 11 | [assembly: AssemblyCopyright("Copyright © The Motley Fool, LLC 2013")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | // Setting ComVisible to false makes the types in this assembly not visible 15 | // to COM components. If you need to access a type in this assembly from 16 | // COM, set the ComVisible attribute to true on that type. 17 | [assembly: ComVisible(false)] 18 | 19 | // The following GUID is for the ID of the typelib if this project is exposed to COM 20 | [assembly: Guid("6c5e852a-babe-4b35-845b-ad259a173483")] 21 | -------------------------------------------------------------------------------- /src/Klondike.WebHost/MetaController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web.Http; 5 | using Lucene.Net.Linq; 6 | using Lucene.Net.Search; 7 | using NuGet; 8 | using NuGet.Lucene; 9 | 10 | namespace Klondike 11 | { 12 | /// 13 | /// Metadata about Klondike. 14 | /// 15 | public class MetaController : ApiController 16 | { 17 | private static readonly ISet Types = new HashSet 18 | { 19 | typeof(MetaController), 20 | typeof(LucenePackage), 21 | typeof(IPackage), 22 | typeof(LuceneDataProvider), 23 | typeof(IndexSearcher) 24 | }; 25 | 26 | /// 27 | /// Gets version information for components of Klondike. 28 | /// 29 | public IDictionary GetComponentVersions() 30 | { 31 | return Types.Select(t => t.Assembly).ToDictionary( 32 | a => a.GetName().Name, 33 | a => a.GetName().Version.ToString()); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /app/templates/packages/advanced-search.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#if error}} 3 |
4 | An error occurred processing the query: {{errorMessage}} 5 |
6 | {{/if}} 7 | 8 |
9 |

10 | Tip: hit ctrl+space to see all search fields. 11 |

12 | 13 |
14 | {{auto-complete-text-input value=query terms=model action="search" class="flex-auto relative"}} 15 | 16 | 17 |
18 |
19 | 20 |

Sample Queries

21 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | {{#each examples as |example|}} 30 | 31 | 32 | 33 | 34 | 35 | {{/each}} 36 | 37 |
QueryDescription 27 |
{{example.query}}{{example.description}}Try it
38 |
39 | -------------------------------------------------------------------------------- /app/routes/packages/view.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ProgressIndicator from 'klondike/progress-indicator'; 3 | import ProgressIndicatorRoute from 'klondike/mixins/progress-indicator-route'; 4 | import describePromise from 'klondike/util/describe-promise'; 5 | 6 | export default Ember.Route.extend(ProgressIndicatorRoute, { 7 | model: function(params) { 8 | return this.findModel('package', params.id, params.version); 9 | }, 10 | 11 | setupController: function(controller, model) { 12 | this._super(controller, model); 13 | 14 | if (!Ember.isEmpty(model.get('versionHistory'))) { 15 | return; 16 | } 17 | 18 | var fullModel = this.findModel('package', model.id, model.version); 19 | 20 | if (fullModel.then) { 21 | ProgressIndicator.start(); 22 | fullModel.then(function(m) { 23 | model.setProperties(m); 24 | ProgressIndicator.done(); 25 | }, null, describePromise(this, 'setupController')); 26 | } else { 27 | model.setProperties(fullModel); 28 | } 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /app/controllers/login.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import BaseControllerMixin from 'klondike/mixins/base-controller'; 3 | 4 | export default Ember.Controller.extend(BaseControllerMixin, { 5 | username: '', 6 | password: '', 7 | errorMessage: '', 8 | isRedirected: false, 9 | 10 | actions: { 11 | logIn: function () { 12 | var self = this; 13 | self.set('errorMessage', ''); 14 | 15 | return this.get('session').logIn(this.get('username'), this.get('password')) 16 | .then(function() { 17 | var previousTransition = self.get('previousTransition'); 18 | 19 | if (previousTransition) { 20 | previousTransition.retry(); 21 | return; 22 | } 23 | 24 | self.transitionToRoute('index'); 25 | }) 26 | .catch(function(error) { 27 | console.debug('authentication failure:', error); 28 | self.set('errorMessage', 'Authentication failed.'); 29 | }); 30 | } 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /app/templates/profile.hbs: -------------------------------------------------------------------------------- 1 |
2 |

{{model.username}}

3 | 4 |
5 | {{#if keyHidden}} 6 |

Your API Key is hidden for security.

7 | 8 | {{else}} 9 | {{#if canPushPackages}} 10 |

Your API Key is:

11 | {{code-snippet content=key}} 12 |

To push packages, first set your API Key:

13 | 14 | {{code-snippet content=setApiKeyCommand prompt='PM> '}} 15 | 16 |

To push packages, run:

17 | 18 | {{code-snippet content=pushPackageCommand prompt='PM> '}} 19 | {{else}} 20 |

Your NuGet API Key is {{key}}, but you are not allowed to push packages.

21 | {{/if}} 22 | 23 | 24 | {{/if}} 25 |
26 | 27 |

28 | You have the following permissions: 29 | 30 |

    31 | {{#each model.roles as |role|}} 32 |
  • {{role}}
  • 33 | {{else}} 34 |
  • (none)
  • 35 | {{/each}} 36 |
37 |

38 |
39 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Klondike 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 | 31 | {{content-for 'body-footer'}} 32 | {{content-for 'test-body-footer'}} 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/controllers/packages/advanced-search.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | query: '', 5 | error: null, 6 | examples: [ 7 | { query: 'Dependencies:Newtonsoft.Json', description: 'Find packages that depend on Newtonsoft.Json' }, 8 | { query: 'Files:Nuget.Core.dll', description: 'Find packages that include Nuget.Core.dll' }, 9 | { query: 'VersionDownloadCount:[1 TO *]', description: 'Find packages that have 1 or more downloads' } 10 | ], 11 | 12 | errorMessage: function() { 13 | return this.get('error.response.message'); 14 | }.property('error'), 15 | 16 | actions: { 17 | search: function () { 18 | var query = this.get('query'); 19 | var route = Ember.isEmpty(query) ? 'packages.list' : 'packages.search'; 20 | this.set('error', null); 21 | this.transitionToRoute(route, {queryParams: {query: query, latestOnly:false, page: 0, includePrerelease: true, originFilter: 'any' }}); 22 | }, 23 | 24 | selectExample: function(query) { 25 | this.set('query', query); 26 | }, 27 | 28 | invalidSearch: function(error) { 29 | this.set('error', error); 30 | } 31 | } 32 | }); 33 | 34 | -------------------------------------------------------------------------------- /src/Klondike.WebHost/IVirtualPathUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Web; 3 | using System.Web.Http; 4 | using System.Xml.Linq; 5 | using Autofac; 6 | using NuGet.Lucene.Web; 7 | using NuGet.Lucene.Web.Formatters; 8 | using Owin; 9 | using Microsoft.Owin; 10 | using System.Web.Hosting; 11 | 12 | namespace Klondike 13 | { 14 | public interface IVirtualPathUtility 15 | { 16 | /// 17 | /// Converts a virtual path to a physical file system path. 18 | /// 19 | string MapPath(string virtualPath); 20 | 21 | /// 22 | /// Converts an app-relative path (e.g. ~/index.html 23 | /// to an absolute URI path (e.g. /myApp/index.html). 24 | /// 25 | string ToAbsolute(string virtualPath); 26 | } 27 | 28 | public class WebHostVirtualPathUtility : IVirtualPathUtility 29 | { 30 | public string MapPath(string virtualPath) 31 | { 32 | return HostingEnvironment.MapPath(virtualPath); 33 | } 34 | 35 | public string ToAbsolute(string virtualPath) 36 | { 37 | return VirtualPathUtility.ToAbsolute(virtualPath); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # http://www.appveyor.com/docs/appveyor-yml 2 | 3 | # Fix line endings in Windows. (runs before repo cloning) 4 | init: 5 | - git config --global core.autocrlf input 6 | 7 | # Test against these versions of Node.js. 8 | environment: 9 | matrix: 10 | - nodejs_version: "0.12" 11 | 12 | install: 13 | # Get the latest stable version of Node 0.STABLE.latest 14 | - cmd: nuget restore 15 | - ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) 16 | - cmd: npm config set spin false 17 | - cmd: npm install -g npm@^2 18 | - cmd: npm install -g ember-cli bower 19 | - cmd: npm install 20 | - cmd: bower install 21 | - cmd: npm run-script release 22 | - ps: Copy-Item .\*.txt .\dist 23 | 24 | # Don't actually build. 25 | build: off 26 | 27 | # Tests are executed elsewhere 28 | test: off 29 | 30 | # Set build version format here instead of in the admin panel. 31 | version: "{build}" 32 | 33 | artifacts: 34 | - path: .\dist 35 | name: Klondike 36 | type: zip 37 | - path: LICENSE.txt 38 | name: License 39 | - path: COPYRIGHT.txt 40 | name: Copyright 41 | 42 | cache: 43 | - packages -> **\packages.config 44 | - node_modules -> package.json 45 | - bower_components -> bower.json 46 | - '%LocalAppData%\NuGet\Cache' 47 | -------------------------------------------------------------------------------- /app/mixins/user-permission-observer.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import describePromise from 'klondike/util/describe-promise'; 3 | 4 | export default Ember.Mixin.create({ 5 | _permissionObservers: [], 6 | 7 | init: function() { 8 | this.set('_permissionObservers', []); 9 | this._super(); 10 | }, 11 | 12 | observeUserPermission: function(property, apiName, method) { 13 | var observer = { property: property, apiName: apiName, method: method }; 14 | this.get('_permissionObservers').push(observer); 15 | this._updateUserPermission(observer); 16 | }, 17 | 18 | _updateUserPermission: function(observer) { 19 | var self = this; 20 | this.get('session').isAllowed(observer.apiName, observer.method).then(function(result) { 21 | var prevResult = self.get(observer.property); 22 | if (result===prevResult) { 23 | return; 24 | } 25 | 26 | self.set(observer.property, result); 27 | return result; 28 | }, null, describePromise(this, '_updateUserPermission')); 29 | }, 30 | 31 | _sessionUserDidChange: Ember.observer('session.user', function() { 32 | this.get('_permissionObservers').forEach(this._updateUserPermission, this); 33 | }) 34 | }); 35 | -------------------------------------------------------------------------------- /src/Klondike.WebHost/AppBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.Owin; 3 | using Owin; 4 | 5 | namespace Klondike 6 | { 7 | public static class AppBuilderExtensions 8 | { 9 | /// 10 | /// Sends a static content file for any request that would otherwise result in a 404. 11 | /// 12 | /// 13 | /// 14 | /// Optional list of path strings that should suppress this behavior. 15 | /// 16 | public static IAppBuilder UseFallbackFile(this IAppBuilder appBuilder, string fallbackFile, params PathString[] blacklistPathStrings) 17 | { 18 | return appBuilder.Use(async (ctx, next) => 19 | { 20 | await next(); 21 | 22 | var path = ctx.Request.Path; 23 | 24 | if (ctx.Response.StatusCode != 404 || blacklistPathStrings.Any(path.StartsWithSegments)) 25 | { 26 | return; 27 | } 28 | 29 | ctx.Response.StatusCode = 200; 30 | 31 | await ctx.Response.SendFileAsync(fallbackFile); 32 | }); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/templates/admin.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Package Index

4 |

5 | If the package directory has been modified outside of Klondike, 6 | it can be synchronized with the Lucene index manually here. 7 |

8 |

9 | Use Rebuild Index to re-index all packages even if they 10 | appear to be unchanged. This process is useful when upgrading Klondike. 11 | All metadata will be recomputed except package download counters, which 12 | will be preserved. 13 |

14 | {{#if canSynchronize}} 15 | 16 | 17 | 18 | {{else}} 19 |
You are not allowed to synchronize packages.
20 | {{/if}} 21 |
22 | 23 |
24 |

Accounts

25 | {{#link-to 'users.list'}}Manage Accounts{{/link-to}} 26 |
27 |
28 | -------------------------------------------------------------------------------- /app/mixins/pagination-support.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Mixin.create({ 4 | hasPaginationSupport: true, 5 | total: 0, 6 | page: 0, 7 | pageSize: 10, 8 | didRequestPage: Ember.K, 9 | 10 | first: function () { 11 | return this.get('page') * this.get('pageSize') + 1; 12 | }.property('page', 'pageSize'), 13 | 14 | last: function () { 15 | return Math.min((this.get('page') + 1) * this.get('pageSize'), this.get('total')); 16 | }.property('page', 'pageSize', 'total'), 17 | 18 | hasPrevious: function () { 19 | return this.get('page') > 0; 20 | }.property('page'), 21 | 22 | hasNext: function () { 23 | return this.get('last') < this.get('total'); 24 | }.property('last', 'total'), 25 | 26 | nextPage: function () { 27 | if (this.get('hasNext')) { 28 | this.incrementProperty('page'); 29 | } 30 | }, 31 | 32 | previousPage: function () { 33 | if (this.get('hasPrevious')) { 34 | this.decrementProperty('page'); 35 | } 36 | }, 37 | 38 | totalPages: function () { 39 | return Math.ceil(this.get('total') / this.get('pageSize')); 40 | }.property('total', 'pageSize'), 41 | 42 | pageDidChange: Ember.observer('page', function () { 43 | this.didRequestPage(this.get('page')); 44 | }) 45 | }); 46 | -------------------------------------------------------------------------------- /app/controllers/profile.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import BaseControllerMixin from 'klondike/mixins/base-controller'; 3 | 4 | export default Ember.Controller.extend(BaseControllerMixin, { 5 | pushApiName: 'packages.putPackage', 6 | pushUriBinding: 'restClient.packageSourceUri', 7 | canPushPackages: false, 8 | keyHidden: true, 9 | 10 | key: function() { 11 | return this.get('keyHidden') ? '(hidden)' : this.get('model.key'); 12 | }.property('keyHidden', 'model.key'), 13 | 14 | setApiKeyCommand: function() { 15 | return 'nuget setApiKey ' + this.get('key') + ' -Source ' + this.get('pushUri'); 16 | }.property('pushUri', 'key'), 17 | 18 | pushPackageCommand: function() { 19 | return 'nuget push [package.nupkg] -Source ' + this.get('pushUri'); 20 | }.property('pushUri'), 21 | 22 | init: function() { 23 | this._super(); 24 | this.sessionUserDidChange(); 25 | }, 26 | 27 | sessionUserDidChange: Ember.observer('session.user', function() { 28 | var self = this; 29 | this.get('session').isAllowed(this.get('pushApiName')).then(function(result) { 30 | self.set('canPushPackages', result); 31 | }); 32 | }), 33 | 34 | actions: { 35 | changeKey: function() { 36 | this.get('session').changeKey(); 37 | }, 38 | 39 | revealKey: function() { 40 | this.set('keyHidden', false); 41 | } 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /app/models/semantic-version.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Object.extend(Ember.Comparable, { 4 | version: '', 5 | 6 | compare: function(a, b) { 7 | /* -1 if a < b 8 | 0 if a == b 9 | 1 if a > b 10 | */ 11 | 12 | var av = a.get('version').split('-'); 13 | var bv = b.get('version').split('-'); 14 | 15 | var vcmp = this.compareSemanticVersion(av[0], bv[0]); 16 | 17 | if (vcmp !== 0) { 18 | return vcmp; 19 | } 20 | 21 | av.shift(); 22 | bv.shift(); 23 | 24 | var ax = av.join('-'); 25 | var bx = bv.join('-'); 26 | 27 | if (!Ember.isEmpty(ax) && Ember.isEmpty(bx)) { return -1; } 28 | if (Ember.isEmpty(ax) && !Ember.isEmpty(bx)) { return 1; } 29 | 30 | return Ember.compare(ax, bx); 31 | }, 32 | 33 | compareSemanticVersion: function(a, b) { 34 | a = a.split('.').map(function(s) { return parseInt(s); }); 35 | b = b.split('.').map(function(s) { return parseInt(s); }); 36 | 37 | var len = Math.max(a.length, b.length); 38 | var x = a.length < len ? a : (b.length < len ? b : null); 39 | 40 | while (x && x.length < len) { 41 | x.push(0); 42 | } 43 | 44 | for (var i=0; i b[i]) { return 1; } 47 | } 48 | 49 | return 0; 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /app/routes/packages/search.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ProgressIndicatorRoute from 'klondike/mixins/progress-indicator-route'; 3 | 4 | export default Ember.Route.extend(ProgressIndicatorRoute, { 5 | queryParams: { 6 | query: { 7 | refreshModel: true 8 | }, 9 | page: { 10 | refreshModel: true 11 | }, 12 | sortBy: { 13 | refreshModel: true 14 | }, 15 | sortOrder: { 16 | refreshModel: true 17 | }, 18 | includePrerelease: { 19 | refreshModel: true 20 | }, 21 | originFilter: { 22 | refreshModel: true 23 | }, 24 | latestOnly: { 25 | refreshModel: true 26 | } 27 | }, 28 | 29 | model: function (params, transition) { 30 | var self = this; 31 | return this.get('packages').search( 32 | params.query || '', 33 | params.page || 0, 34 | /* page size */ undefined, 35 | params.sortBy, 36 | params.sortOrder, 37 | params.includePrerelease, 38 | params.originFilter, 39 | params.latestOnly 40 | ).catch(function(error) { 41 | if (error && error.status === 400) { 42 | self.done(); 43 | self.send('invalidSearch', error); 44 | transition.abort(); 45 | return; 46 | } 47 | throw error; 48 | }); 49 | }, 50 | 51 | afterModel: function(results) { 52 | if (results.get('totalHits') === 1) { 53 | this.transitionTo('packages.view', results.get('hits')[0]); 54 | } 55 | }, 56 | }); 57 | -------------------------------------------------------------------------------- /app/adapters/user.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import User from '../models/user'; 3 | import describePromise from 'klondike/util/describe-promise'; 4 | 5 | export default Ember.Object.extend({ 6 | find: function(id) { 7 | var self = this; 8 | var query = this.get('restClient').ajax('users.get', { data: { username: id } }); 9 | return query.then(function(json) { 10 | return self.createModel(json); 11 | }, null, describePromise(this, 'find', arguments)); 12 | }, 13 | 14 | createModel: function(params) { 15 | return User.create(params || {}); 16 | }, 17 | 18 | list: function() { 19 | var self = this; 20 | return this.get('restClient').ajax('users.getAllUsers').then(function(json) { 21 | return json.map(function(user) { 22 | return self.createModel(user); 23 | }); 24 | }, null, describePromise(this, 'list')); 25 | }, 26 | 27 | add: function(user) { 28 | user.overwrite = false; 29 | return this._save(user, 'users.put'); 30 | }, 31 | 32 | update: function(user) { 33 | return this._save(user, 'users.post'); 34 | }, 35 | 36 | delete: function(username) { 37 | return this.get('restClient').ajax('users.delete', { data: {username: username} }); 38 | }, 39 | 40 | _save: function(user, apiName) { 41 | var self = this; 42 | return self.get('restClient').ajax(apiName, { data: user }); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "klondike", 3 | "version": "2.1.0", 4 | "private": true, 5 | "directories": { 6 | "doc": "doc", 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "start": "ember server", 11 | "build": "ember build", 12 | "release": "ember build --environment=production", 13 | "test": "ember test" 14 | }, 15 | "repository": "https://github.com/themotleyfool/Klondike", 16 | "engines": { 17 | "node": ">= 0.10.0" 18 | }, 19 | "author": "", 20 | "license": "APL-2.0", 21 | "devDependencies": { 22 | "broccoli-asset-rev": "^2.2.0", 23 | "broccoli-msbuild": "git://github.com/chriseldredge/broccoli-msbuild.git#0.2.1", 24 | "broccoli-sass": "^0.5.0", 25 | "broccoli-select": "^0.1.0", 26 | "ember-cli": "1.13.13", 27 | "ember-cli-app-version": "^1.0.0", 28 | "ember-cli-babel": "^5.1.5", 29 | "ember-cli-content-security-policy": "0.4.0", 30 | "ember-cli-dependency-checker": "^1.1.0", 31 | "ember-cli-htmlbars": "^1.0.1", 32 | "ember-cli-htmlbars-inline-precompile": "^0.3.1", 33 | "ember-cli-ic-ajax": "0.2.4", 34 | "ember-cli-inject-live-reload": "^1.3.1", 35 | "ember-cli-qunit": "^1.0.4", 36 | "ember-cli-release": "0.2.8", 37 | "ember-cli-sri": "^1.2.0", 38 | "ember-cli-uglify": "^1.2.0", 39 | "ember-disable-proxy-controllers": "^1.0.1", 40 | "ember-export-application-global": "^1.0.4", 41 | "express": "^4.8.5", 42 | "glob": "^5.0.13", 43 | "http-proxy": "^1.3.0", 44 | "rimraf": "2.2.8", 45 | "rsvp": "^4.2.23" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/components/checkbox-group-item.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | tagName: 'li', 5 | selectionBinding: 'parentView.selection', 6 | 7 | checked: function () { 8 | return this.get('selection').contains(this.get('value')); 9 | }.property('selection', 'value'), 10 | 11 | init: function() { 12 | this.on('change', this._updateSelection); 13 | this.labelPathDidChange(); 14 | this.valuePathDidChange(); 15 | 16 | return this._super(); 17 | }, 18 | 19 | labelPathDidChange: Ember.observer('parentView.checkboxLabelPath', function () { 20 | var labelPath = this.get('parentView.checkboxLabelPath'); 21 | 22 | if (!labelPath) { return; } 23 | 24 | Ember.defineProperty(this, 'label', function () { 25 | return this.get(labelPath); 26 | }.property(labelPath)); 27 | }), 28 | 29 | valuePathDidChange: Ember.observer('parentView.checkboxValuePath', function () { 30 | var labelPath = this.get('parentView.checkboxValuePath'); 31 | 32 | if (!labelPath) { return; } 33 | 34 | Ember.defineProperty(this, 'value', function () { 35 | return this.get(labelPath); 36 | }.property(labelPath)); 37 | }), 38 | 39 | _updateSelection: function (e) { 40 | var val = this.get('value'); 41 | if (e.target.checked) { 42 | this.get('selection').pushObject(val); 43 | } else { 44 | this.get('selection').removeObject(val); 45 | } 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /app/templates/symbols.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Symbol Server

3 | 4 | {{#if model.enabled}} 5 |

6 | Klondike is configured to process NuGet symbol packages 7 | and serve symbols and source code to your debugger. 8 |

9 | 10 |

11 | Point Visual Studio to this symbol server: 12 |

13 | 14 | {{code-snippet content=model.symbolServer}} 15 | 16 |

17 | See this guide from 18 | SymbolSource for more details on how 19 | to configure Visual Studio. 20 |

21 | 22 |

23 | {{#if model.symbolsAvailable}} 24 | Some packages already have symbols. Debug away! 25 | {{else}} 26 | No symbol packages have been pushed to Klondike. 27 | You'll need to do this before attempting to use Klondike 28 | as a symbol server. 29 | {{/if}} 30 |

31 | {{else}} 32 |

33 | Klondike can be configured to process NuGet symbol packages, 34 | but you need to install Debugging Tools for Windows and tell 35 | Klondike where to find them by editing debuggingToolsPath 36 | in Settings.config. 37 |

38 | 39 |

40 | The path should contain the symstore.exe utility. 41 |

42 | {{/if}} 43 | 44 |
45 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Klondike.SelfHost.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Klondike.SelfHost.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("0a2bcf35-0947-4ba0-a3b8-3e352085c1fe")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /app/stores/main.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ApplicationException from 'klondike/application-exception'; 3 | import describePromise from 'klondike/util/describe-promise'; 4 | 5 | var cache = {}; 6 | 7 | function createHandler(funcName) { 8 | return function() { 9 | var name = arguments[0]; 10 | var args = Array.prototype.slice.call(arguments, 1); 11 | 12 | var adapter = this.container.lookup('adapter:' + name); 13 | if (!adapter) { 14 | throw new ApplicationException('The adapter "' + name + '" is not registered in container.'); 15 | } 16 | 17 | var func = adapter[funcName]; 18 | if (func === undefined) { 19 | throw new ApplicationException('The adapter "' + name + '" does not have a method named "' + funcName + '"'); 20 | } 21 | 22 | return func.apply(adapter, args); 23 | }; 24 | } 25 | 26 | export default Ember.Object.extend({ 27 | find: function() { 28 | var name = arguments[0]; 29 | var args = Array.prototype.slice.call(arguments, 1); 30 | var id = args.join('::'); 31 | 32 | if (cache[name] && cache[name][id]) { 33 | return cache[name][id]; 34 | } 35 | 36 | var adapter = this.container.lookup('adapter:' + name); 37 | return adapter.find.apply(adapter, args).then(function(record) { 38 | cache[name] = cache[name] || {}; 39 | cache[name][id] = record; 40 | return record; 41 | }, null, describePromise(this, 'find', arguments) + ': Cache Result'); 42 | }, 43 | 44 | list: createHandler('list'), 45 | 46 | createModel: createHandler('createModel'), 47 | }); 48 | -------------------------------------------------------------------------------- /app/controllers/packages/search.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import BaseControllerMixin from 'klondike/mixins/base-controller'; 3 | import PaginationSupportMixin from 'klondike/mixins/pagination-support'; 4 | 5 | export default Ember.Controller.extend(BaseControllerMixin, PaginationSupportMixin, { 6 | queryParams: ['query', 'page', 'sortBy', 'sortOrder', 'includePrerelease', 'originFilter', 'latestOnly'], 7 | 8 | originFilters: [ 9 | { value: 'any', label: 'Local and mirrored'}, 10 | { value: 'local', label: 'Local'}, 11 | { value: 'mirror', label: 'Mirrored'} 12 | ], 13 | 14 | versionFilters: [ 15 | { value: false, label: 'Stable only'}, 16 | { value: true, label: 'Include pre-release'} 17 | ], 18 | 19 | sortByColumns: [ 20 | { value: 'score', label: 'Sort by relevance'}, 21 | { value: 'title', label: 'Sort by title'}, 22 | { value: 'id', label: 'Sort by package ID'}, 23 | { value: 'published', label: 'Sort by date published'} 24 | ], 25 | 26 | latestOnlyFilters: [ 27 | { value: true, label: 'Latest version'}, 28 | { value: false, label: 'All versions'} 29 | ], 30 | 31 | query: '', 32 | pageBinding: 'model.page', 33 | sortBy: 'score', 34 | sortOrder: 'ascending', 35 | includePrerelease: false, 36 | originFilter: 'any', 37 | latestOnly: true, 38 | 39 | total: Ember.computed.oneWay('model.totalHits'), 40 | 41 | actions: { 42 | 'nextPage': function() { 43 | this.set('page', this.get('page') + 1); 44 | }, 45 | 'previousPage': function () { 46 | this.set('page', this.get('page') - 1); 47 | }, 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import BaseControllerMixin from 'klondike/mixins/base-controller'; 3 | 4 | export default Ember.Controller.extend(BaseControllerMixin, { 5 | 'packages/search': Ember.inject.controller(), 6 | searchBox: Ember.computed.oneWay('packages/search.query'), 7 | 8 | isLoggedIn: Ember.computed.oneWay('session.isLoggedIn'), 9 | username: Ember.computed.oneWay('session.username'), 10 | isSessionInitialized: Ember.computed.oneWay('session.isInitialized'), 11 | 12 | apiURL: Ember.computed.oneWay('restClient.apiURL'), 13 | 14 | productVersion: Ember.computed('application.version', function() { 15 | return this.get('application.version').split('+')[0]; 16 | }), 17 | 18 | productRevisionHash: Ember.computed('application.version', function() { 19 | var parts = this.get('application.version').split('+'); 20 | if (parts.length === 2) { 21 | return parts[1]; 22 | } 23 | return undefined; 24 | }), 25 | 26 | actions: { 27 | search: function () { 28 | var query = this.get('searchBox'); 29 | var route = Ember.isEmpty(query) ? 'packages.list' : 'packages.search'; 30 | this.transitionToRoute(route, {queryParams: {query: query, page: 0}}); 31 | }, 32 | logIn: function() { 33 | var self = this; 34 | this.get('session').tryLogIn().then(function(success) { 35 | if (!success) { 36 | self.transitionToRoute('login'); 37 | } 38 | }); 39 | }, 40 | logOut: function() { 41 | this.get('session').logOut(); 42 | } 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /app/controllers/packages/view.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import BaseControllerMixin from 'klondike/mixins/base-controller'; 3 | 4 | export default Ember.Controller.extend(BaseControllerMixin, { 5 | origin: function() { 6 | var dataUrl = this.get('model.originUrl') || ''; 7 | var index = dataUrl.indexOf('/api/'); 8 | return index < 0 ? dataUrl : dataUrl.substring(0, index + 1); 9 | }.property('model.originUrl'), 10 | 11 | hasAdditionalDescription: function() { 12 | return this.get('model.summary') !== this.get('model.description'); 13 | }.property('model.summary', 'model.description'), 14 | 15 | installCommand: function() { 16 | return 'Install-Package ' + this.get('model.id') + 17 | ' -Version ' + this.get('model.version') + 18 | ' -Source ' + this.get('packageSourceUri'); 19 | }.property('model.id', 'model.version', 'packageSourceUri'), 20 | 21 | sortColumn: 'semanticVersion', 22 | 23 | actions: { 24 | sortVersions: function(column) { 25 | var arr = this.get('model.versionHistory'); 26 | var prevSortColumn = this.get('sortColumn'); 27 | 28 | if (prevSortColumn === column) { 29 | arr = arr.copy().reverse(); 30 | } else { 31 | arr = arr.sortBy(column).reverse(); 32 | this.set('sortColumn', column); 33 | } 34 | 35 | this.set('model.versionHistory', arr); 36 | }, 37 | 38 | goToDependency: function(dep) { 39 | var includePrerelease = this.get('model.isPrerelease'); 40 | var query = 'PackageId:' + dep.id; 41 | 42 | this.transitionToRoute('packages.search', {queryParams: {query: query, latestOnly:true, page: 0, includePrerelease: includePrerelease, originFilter: 'any' }}); 43 | } 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /app/templates/users/edit.hbs: -------------------------------------------------------------------------------- 1 |
2 |

{{actionLabel}} Account

3 | 4 |
5 |
6 |
7 | 8 | {{focus-input type="text" value=model.username class="field col-12 block"}} 9 |
10 |
11 | 12 | {{input type="text" value=model.key placeholder="(Leave blank to generate)" class="field col-12 block"}} 13 |
14 |
15 | 16 |
17 |

Permissions

18 | {{checkbox-group 19 | name="roles" 20 | selection=model.roles 21 | content=model.allRoles 22 | checkboxLabelPath="content.label" 23 | checkboxValuePath="content.name" 24 | classNames="list-reset"}} 25 |
26 | 27 |
28 | 29 | 30 | {{#if canDelete}} 31 | 32 | {{/if}} 33 | 34 | 35 |
36 | 37 | {{#if errorMessage}} 38 |
{{errorMessage}}
39 | {{/if}} 40 | 41 | {{#if isSaving}} 42 |
Saving...
43 | {{else}} 44 | {{#if isSaveCompleted}} 45 |
Saved.
46 | {{/if}} 47 | {{/if}} 48 |
49 |
50 | -------------------------------------------------------------------------------- /app/services/hubs.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ApplicationException from 'klondike/application-exception'; 3 | import signalR from './signalR'; 4 | import describePromise from 'klondike/util/describe-promise'; 5 | 6 | export default Ember.Service.extend({ 7 | _resolveHubs: null, 8 | 9 | init: function() { 10 | var restClient = this.get('restClient'); 11 | 12 | if (!restClient) { 13 | throw new ApplicationException('Must set restClient property on hub'); 14 | } 15 | 16 | var url; 17 | var loadHubs = restClient.getApi('Indexing.Hub').then(function (hubApi) { 18 | url = hubApi.href; 19 | var hubUrl = url + '/hubs'; 20 | 21 | return Ember.$.ajax(hubUrl).fail(function(xhr, status) { 22 | throw new ApplicationException('Failed to load SignalR hubs at ' + hubUrl + ': ' + status + ' (' + xhr.status + ')'); 23 | }); 24 | }, null, describePromise(this, 'init') + ': Load SignalR/hubs.js'); 25 | 26 | var resolveHubs = loadHubs.then(function() { 27 | var hubs = {}; 28 | 29 | for (var i in signalR) { 30 | var prop = signalR[i]; 31 | if (typeof prop === 'object' && 'hubName' in prop) { 32 | hubs[prop.hubName] = prop; 33 | } 34 | } 35 | 36 | signalR.hub.url = url; 37 | 38 | return hubs; 39 | }, null, describePromise(this, 'init') + ': Resolve Hubs'); 40 | 41 | this.set('_resolveHubs', resolveHubs); 42 | }, 43 | 44 | getHub: function(hubName) { 45 | return this.get('_resolveHubs').then(function(hubs) { 46 | var hub = hubs[hubName]; 47 | 48 | if (!hub) { 49 | throw new ApplicationException('No such hub: ' + hubName); 50 | } 51 | 52 | return hub; 53 | }, null, describePromise(this, 'getHub', arguments)); 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /app/services/package-indexer.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import UserPermissionObserver from 'klondike/mixins/user-permission-observer'; 3 | import signalR from './signalR'; 4 | import describePromise from 'klondike/util/describe-promise'; 5 | 6 | export default Ember.Service.extend(UserPermissionObserver, { 7 | hubs: null, 8 | 9 | status: {}, 10 | statusHub: null, 11 | 12 | canSynchronize: false, 13 | 14 | init: function () { 15 | this._super(); 16 | var self = this; 17 | 18 | this.get('hubs').getHub('status').then(function(statusHub) { 19 | self.set('statusHub', statusHub); 20 | }, null, describePromise(this, 'init')); 21 | 22 | this.observeUserPermission('canSynchronize', 'indexing.synchronize'); 23 | }, 24 | 25 | statusHubDidChange: Ember.observer('statusHub', function() { 26 | var self = this; 27 | var setStatusCallback = function (status) { 28 | self.set('status', status); 29 | }; 30 | 31 | var hub = this.get('statusHub'); 32 | 33 | hub.client.updateStatus = setStatusCallback; 34 | 35 | hub.connection.stateChanged(function (change) { 36 | var isConnected = change.newState === signalR.connectionState.connected; 37 | self.set('isConnected', isConnected); 38 | 39 | if (isConnected) { 40 | hub.server.getStatus().then(setStatusCallback); 41 | } else { 42 | setStatusCallback({}); 43 | } 44 | }); 45 | 46 | signalR.hub.start({ waitForPageLoad: false }); 47 | }), 48 | 49 | rebuild: function () { 50 | this.get('restClient').ajax('indexing.synchronize', { data: { mode: 'complete' } }); 51 | }, 52 | 53 | synchronize: function () { 54 | this.get('restClient').ajax('indexing.synchronize'); 55 | }, 56 | 57 | cancel: function () { 58 | this.get('restClient').ajax('indexing.cancel'); 59 | }, 60 | 61 | isRunning: function () { 62 | return this.status.synchronizationState !== 'Idle'; 63 | }.property('status'), 64 | }); 65 | -------------------------------------------------------------------------------- /app/mixins/authorized-route.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Mixin.create({ 4 | authorizedApiName: '', 5 | 6 | beforeModel: function(transition) { 7 | if (!this.get('authorizedApiName')) { 8 | var message = 'must set authorizedApiName property when using AuthorizedRoute mixin'; 9 | console.error(message); 10 | transition.abort(); 11 | return; 12 | } 13 | 14 | var self = this; 15 | 16 | var session = this.get('session'); 17 | return session.then(function() { 18 | if (!session.get('isLoggedIn')) { 19 | var loginController = self.controllerFor('login'); 20 | loginController.set('previousTransition', transition); 21 | loginController.set('isRedirected', true); 22 | transition.abort(); 23 | self.transitionTo('login'); 24 | return; 25 | } 26 | 27 | return session.isAllowed(self.get('authorizedApiName')).then(function(result) { 28 | if (!result) { 29 | transition.abort(); 30 | self.transitionTo('denied'); 31 | } 32 | }); 33 | }); 34 | }, 35 | 36 | activate: function() { 37 | var self = this; 38 | var session = this.get('session'); 39 | 40 | var observer = function() { 41 | Ember.run.once(function() { 42 | if (!session.get('isLoggedIn')) { 43 | self.transitionTo('index'); 44 | } else { 45 | session.isAllowed(self.get('authorizedApiName')).then(function(result) { 46 | if (!result) { 47 | self.transitionTo('denied'); 48 | } 49 | }); 50 | } 51 | }); 52 | }; 53 | 54 | this.set('_sessionUserObserver', observer); 55 | session.addObserver('user', observer); 56 | }, 57 | 58 | deactivate: function() { 59 | var session = this.get('session'); 60 | session.removeObserver('user', this.get('_sessionUserObserver')); 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /src/Klondike.WebHost/Startup.cs: -------------------------------------------------------------------------------- 1 | using Common.Logging; 2 | using System; 3 | using System.Web.Http; 4 | using Autofac; 5 | using NuGet.Lucene.Web; 6 | using NuGet.Lucene.Web.Formatters; 7 | using Owin; 8 | using Microsoft.Owin; 9 | 10 | namespace Klondike 11 | { 12 | public class Startup : NuGet.Lucene.Web.Startup 13 | { 14 | IContainer container; 15 | 16 | protected override INuGetWebApiSettings CreateSettings() 17 | { 18 | return new NuGetWebApiWebHostSettings(prefix: ""); 19 | } 20 | 21 | protected override void Start(IAppBuilder app, IContainer container) 22 | { 23 | var pathUtility = container.Resolve(); 24 | app.UseFallbackFile(pathUtility.MapPath("~/index.html"), new PathString("/api"), new PathString("/assets")); 25 | 26 | base.Start(app, container); 27 | } 28 | 29 | protected override HttpConfiguration CreateHttpConfiguration() 30 | { 31 | return new HttpConfiguration(new HttpRouteCollection(GlobalConfiguration.Configuration.VirtualPathRoot)); 32 | } 33 | 34 | protected override IContainer CreateContainer(IAppBuilder app) 35 | { 36 | var builder = new ContainerBuilder(); 37 | builder.RegisterType().As(); 38 | builder.RegisterType().As(); 39 | container = base.CreateContainer(app); 40 | builder.Update(container); 41 | return container; 42 | } 43 | 44 | protected override void RegisterServices(IContainer container, IAppBuilder app, HttpConfiguration config) 45 | { 46 | var apiMapper = container.Resolve(); 47 | base.RegisterServices(container, app, config); 48 | config.Routes.MapHttpRoute("Version", apiMapper.PathPrefix + "version", new { controller = "Meta" }); 49 | } 50 | 51 | protected override NuGetHtmlMicrodataFormatter CreateMicrodataFormatter() 52 | { 53 | return container.Resolve(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/initializers/dependencies.js: -------------------------------------------------------------------------------- 1 | import Session from 'klondike/session'; 2 | 3 | export default { 4 | name: 'inject-dependencies', 5 | initialize: function(container, app) { 6 | // restClient 7 | app.inject('adapter', 'restClient', 'service:rest-client'); 8 | app.inject('session', 'restClient', 'service:rest-client'); 9 | app.inject('service:hubs', 'restClient', 'service:rest-client'); 10 | app.inject('service:package-indexer', 'restClient', 'service:rest-client'); 11 | app.inject('controller', 'restClient', 'service:rest-client'); 12 | app.inject('route', 'restClient', 'service:rest-client'); 13 | 14 | // session 15 | app.register('session:main', Session); 16 | app.inject('route', 'session', 'session:main'); 17 | app.inject('controller', 'session', 'session:main'); 18 | app.inject('service:package-indexer', 'session', 'session:main'); 19 | 20 | // store 21 | app.inject('route', 'store', 'store:main'); 22 | app.inject('controller', 'store', 'store:main'); 23 | app.inject('session', 'store', 'store:main'); 24 | 25 | // hubs 26 | app.inject('service:package-indexer', 'hubs', 'service:hubs'); 27 | 28 | // package-indexer 29 | app.inject('controller:application', 'indexer', 'service:package-indexer'); 30 | app.inject('controller:admin', 'indexer', 'service:package-indexer'); 31 | app.inject('route:admin', 'indexer', 'service:package-indexer'); 32 | 33 | // package adapter 34 | app.inject('route:packages.list', 'packages', 'adapter:package'); 35 | app.inject('route:packages.search', 'packages', 'adapter:package'); 36 | app.inject('route:packages.advanced-search', 'packages', 'adapter:package'); 37 | app.inject('route:packages.view', 'packages', 'adapter:package'); 38 | 39 | // user adapter 40 | app.inject('controller:users.add', 'users', 'adapter:user'); 41 | app.inject('controller:users.edit', 'users', 'adapter:user'); 42 | 43 | // application 44 | app.inject('service:rest-client', 'application', 'application:main'); 45 | app.inject('session:main', 'application', 'application:main'); 46 | app.inject('controller:application', 'application', 'application:main'); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | // Here you can point to an external Klondike API provider: 5 | var app = { 6 | apiURL: '', 7 | apiKey: '' 8 | }; 9 | 10 | var ENV = { 11 | modulePrefix: 'klondike', 12 | environment: environment, 13 | configuration: 'Debug', 14 | baseURL: '/', 15 | locationType: 'auto', 16 | EmberENV: { 17 | FEATURES: { 18 | // Here you can enable experimental features on an ember canary build 19 | // e.g. 'with-controller': true 20 | } 21 | } 22 | }; 23 | 24 | if (app.apiURL === '') { 25 | var apiURL = ENV.baseURL; 26 | if (apiURL[apiURL.length-1] !== '/') 27 | { 28 | apiURL += '/'; 29 | } 30 | app.apiURL = apiURL + 'api/'; 31 | } else if (app.apiURL[app.apiURL.length-1] !== '/') { 32 | app.apiURL += '/'; 33 | } 34 | 35 | var cspExtra = app.apiURL.indexOf('http') === 0 ? ' ' + app.apiURL : ''; 36 | 37 | ENV.APP = app; 38 | 39 | ENV.contentSecurityPolicy = { 40 | 'default-src': "'none'", 41 | 'script-src': "'self'" + cspExtra, 42 | 'font-src': "'self'", 43 | 'connect-src': "'self'" + cspExtra + cspExtra.replace('http://', 'ws://'), 44 | 'img-src': "*", 45 | 'style-src': "'self'", 46 | 'object-src': "'self'", 47 | 'media-src': "'self'" 48 | } 49 | 50 | if (environment === 'development' || environment === 'ember-only') { 51 | // ENV.APP.LOG_RESOLVER = true; 52 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 53 | // ENV.APP.LOG_TRANSITIONS = true; 54 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 55 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 56 | ENV.contentSecurityPolicy['script-src'] = ENV.contentSecurityPolicy['script-src'] + " 'unsafe-eval'"; 57 | } 58 | 59 | if (environment === 'test') { 60 | // Testem prefers this... 61 | ENV.baseURL = '/'; 62 | ENV.locationType = 'none'; 63 | 64 | // keep test console output quieter 65 | ENV.APP.LOG_ACTIVE_GENERATION = false; 66 | ENV.APP.LOG_VIEW_LOOKUPS = false; 67 | 68 | ENV.APP.rootElement = '#ember-testing'; 69 | } 70 | 71 | if (environment === 'ember-only') { 72 | ENV.disableMSBuild = true; 73 | } 74 | 75 | if (environment === 'production') { 76 | ENV.configuration = 'Release'; 77 | } 78 | 79 | return ENV; 80 | }; 81 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost/README.md: -------------------------------------------------------------------------------- 1 | # Klondike.SelfHost 2 | 3 | This app allows Klondike to run without IIS from the command line or as a service. 4 | 5 | ## Settings 6 | 7 | Settings are configured in [Settings.config](../Klondike.WebHost/Settings.config) and can be overridden on the command line. 8 | 9 | In addition to the options found in Settings.config, the following additional options are supported: 10 | 11 | Switch | Default | Description 12 | ------------------------------------- | ---------- | ----------- 13 | port | 8080 | When no url(s) are specified, listens on all interface on this tcp port. 14 | url | | URL to listen on, e.g. `http://example.com/` (may be repeated for multiple bindings). 15 | virtualPathRoot | / | Virtual path root to prefix all routes with. 16 | serverFactory | Nowin | Selects OWIN server factory (may also use Microsoft.Owin.Host.HttpListener on Windows). 17 | enableAnonymousAuthentication | true | When using Microsoft.Owin.Host.HttpListener, enables / disables Anonymous authentication. 18 | enableIntegratedWindowsAuthentication | false | When using Microsoft.Owin.Host.HttpListener, enables / disables Windows authentication. 19 | baseDirectory | (computed) | The directory where Klondike.SelfHost.exe resides, or the parent of `bin` when in a bin folder. 20 | interactive | (computed) | When set to true, don't run as a service, block on Console.ReadLine. When unspecified, uses `Environment.UserInteractive`. 21 | 22 | ## Running as a Service 23 | 24 | You can install Klondike as a Windows Service so it runs in the background and starts when Windows starts: 25 | 26 | ``` 27 | C:\Windows\system32\sc.exe create Klondike \ 28 | start= auto 29 | binpath= "c:\path\to\klondike\bin\Klondike.SelfHost.exe --args --go --here" 30 | ``` 31 | 32 | You can also run the service using `mono-service` on Unix hosts: 33 | 34 | ``` 35 | mono /path/to/mono-service.exe Klondike.SelfHost.exe 36 | ``` 37 | 38 | *N.B.*: Make sure to use the correct version of mono-service.exe. The one on your system path may be outdated. 39 | 40 | *N.B.*: Also make sure that Klondike's `bin` directory is used as the working directory when using `mono-service`. 41 | 42 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost/CommandLineSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Klondike.SelfHost 6 | { 7 | public class CommandLineSettings 8 | { 9 | private readonly ILookup values; 10 | 11 | public CommandLineSettings(ILookup values) 12 | { 13 | this.values = values; 14 | } 15 | 16 | public static CommandLineSettings Parse(string[] args) 17 | { 18 | var values = new List>(); 19 | 20 | foreach (var arg in args) 21 | { 22 | int keyBegin; 23 | 24 | if (arg.StartsWith("--")) 25 | { 26 | keyBegin = 2; 27 | } 28 | else if (arg.StartsWith("-") || arg.StartsWith("/")) 29 | { 30 | keyBegin = 1; 31 | } 32 | else 33 | { 34 | continue; 35 | } 36 | 37 | var keyEnd = arg.IndexOf('='); 38 | 39 | if (keyEnd > 0) 40 | { 41 | values.Add(new KeyValuePair(arg.Substring(keyBegin, keyEnd - keyBegin), arg.Substring(keyEnd + 1))); 42 | } 43 | else 44 | { 45 | values.Add(new KeyValuePair(arg.Substring(keyBegin), "true")); 46 | } 47 | 48 | } 49 | 50 | return new CommandLineSettings(values.ToLookup(k => k.Key, k => k.Value, StringComparer.InvariantCultureIgnoreCase)); 51 | } 52 | 53 | public T Get(string key) 54 | { 55 | return (T) Convert.ChangeType(values[key].First(), typeof (T)); 56 | } 57 | 58 | public T GetValueOrDefault(string key, T defaultValue) 59 | { 60 | var value = values[key].FirstOrDefault(); 61 | if (value != null) 62 | { 63 | return (T) Convert.ChangeType(value, typeof (T)); 64 | } 65 | return defaultValue; 66 | } 67 | 68 | public IEnumerable GetValues(string key) 69 | { 70 | return values[key].Select(k => (T) Convert.ChangeType(k, typeof(T))); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{partial "svg-defs"}} 2 | 3 |
4 | 31 | 32 |
33 | {{outlet}} 34 |
35 | 36 |
37 |
38 |
    39 | {{#with indexer.status as |stats|}} 40 |
  • 41 | Total Packages: {{stats.totalPackages}} 42 |
  • 43 |
  • 44 | Synchronizer State: 45 | {{stats.synchronizationState}} 46 | {{#if stats.packagesToIndex}} 47 | {{stats.completedPackages}} / {{stats.packagesToIndex}} 48 | {{/if}} 49 |
  • 50 | {{/with}} 51 |
  • 52 | Klondike: 53 | {{productVersion}} 54 | {{#if productRevisionHash}} 55 | ({{productRevisionHash}}) 56 | {{/if}} 57 |
  • 58 |
59 |
60 |
61 | 62 |
63 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost/SelfHostVirtualPathUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using Autofac; 5 | using Microsoft.Owin; 6 | using Microsoft.Owin.FileSystems; 7 | using Microsoft.Owin.StaticFiles; 8 | using NuGet.Lucene.Web; 9 | using NuGet.Lucene.Web.Formatters; 10 | using Owin; 11 | 12 | namespace Klondike.SelfHost 13 | { 14 | public class SelfHostVirtualPathUtility : IVirtualPathUtility 15 | { 16 | readonly string virtualPathRoot; 17 | readonly string webRoot; 18 | 19 | public SelfHostVirtualPathUtility(string webRoot, string virtualPathRoot) 20 | { 21 | if (string.IsNullOrEmpty(webRoot)) 22 | { 23 | throw new ArgumentException("webRoot must not be null or blank", "webRoot"); 24 | } 25 | if (!Path.IsPathRooted(webRoot)) 26 | { 27 | throw new ArgumentException("webRoot must be an absolute path", "webRoot"); 28 | } 29 | if (!Directory.Exists(webRoot)) 30 | { 31 | throw new ArgumentException("Directory does not exist", "webRoot"); 32 | } 33 | 34 | if (virtualPathRoot == null) 35 | { 36 | virtualPathRoot = ""; 37 | } 38 | 39 | if (virtualPathRoot.Length > 0 && !virtualPathRoot.StartsWith("/")) 40 | { 41 | throw new ArgumentException("virtualPathRoot must be blank or start with /", "virtualPathRoot"); 42 | } 43 | 44 | this.webRoot = webRoot.TrimEnd('\\', '/'); 45 | this.virtualPathRoot = virtualPathRoot.TrimEnd('\\', '/'); 46 | } 47 | 48 | public string MapPath(string virtualPath) 49 | { 50 | if (virtualPath.StartsWith("~/")) 51 | { 52 | virtualPath = virtualPath.Substring(1).TrimStart('/'); 53 | } 54 | 55 | return Path.Combine(webRoot, virtualPath); 56 | } 57 | 58 | public string ToAbsolute(string virtualPath) 59 | { 60 | var physicalPath = MapPath(virtualPath); 61 | 62 | if (!physicalPath.StartsWith(webRoot)) 63 | { 64 | throw new InvalidOperationException("Path is not rooted in document root"); 65 | } 66 | 67 | var relativePath = physicalPath.Substring(webRoot.Length); 68 | 69 | return virtualPathRoot + relativePath.Replace('\\', '/'); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Klondike.WebHost/KlondikeHtmlMicrodataFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Web; 8 | using System.Web.Hosting; 9 | using System.Xml.Linq; 10 | using NuGet.Lucene.Web.Formatters; 11 | 12 | namespace Klondike 13 | { 14 | public class KlondikeHtmlMicrodataFormatter : NuGetHtmlMicrodataFormatter 15 | { 16 | readonly Lazy> cssLazy; 17 | readonly IVirtualPathUtility virtualPathUtility; 18 | 19 | public KlondikeHtmlMicrodataFormatter(IVirtualPathUtility virtualPathUtility) 20 | { 21 | SupportedMediaTypes.Add(new MediaTypeWithQualityHeaderValue("application/xml")); 22 | SupportedMediaTypes.Add(new MediaTypeWithQualityHeaderValue("text/xml")); 23 | 24 | Settings.Indent = true; 25 | 26 | Title = "Klondike API"; 27 | 28 | cssLazy = new Lazy>(FindSylesheets); 29 | this.virtualPathUtility = virtualPathUtility; 30 | } 31 | 32 | public override IEnumerable BuildHeadElements(object value, HttpRequestMessage request) 33 | { 34 | var headElements = base.BuildHeadElements(value, request); 35 | 36 | var cssLinks = cssLazy.Value.Select( 37 | i => new XElement("link", 38 | new XAttribute("rel", "stylesheet"), 39 | new XAttribute("href", i))); 40 | 41 | var scripts = new[] 42 | { 43 | new XElement("script", 44 | new XAttribute("src", virtualPathUtility.ToAbsolute("~/js/formtemplate.min.js")), 45 | new XText("")) 46 | }; 47 | 48 | return headElements.Union(cssLinks).Union(scripts); 49 | } 50 | 51 | private IEnumerable FindSylesheets() 52 | { 53 | const string stylePath = "~/assets/"; 54 | var cssDir = virtualPathUtility.MapPath(stylePath); 55 | 56 | if (!Directory.Exists(cssDir)) 57 | { 58 | return new string[0]; 59 | } 60 | 61 | var vendorCss = Directory.GetFiles(cssDir, "vendor*.css").FirstOrDefault(); 62 | var appCss = Directory.GetFiles(cssDir, "klondike*.css").FirstOrDefault(); 63 | return new[] {vendorCss, appCss} 64 | .Where(i => i != null) 65 | .Select(i => virtualPathUtility.ToAbsolute(stylePath + Path.GetFileName(i))) 66 | .ToList(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost/KlondikeService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.ServiceProcess; 4 | using Common.Logging; 5 | using Microsoft.Owin.Hosting; 6 | using NuGet; 7 | 8 | namespace Klondike.SelfHost 9 | { 10 | class KlondikeService : ServiceBase 11 | { 12 | private static readonly ILog Log = LogManager.GetLogger(); 13 | 14 | private readonly SelfHostSettings settings; 15 | private SelfHostStartup startup; 16 | private IDisposable server; 17 | private bool interactive; 18 | 19 | public KlondikeService(SelfHostSettings settings) 20 | { 21 | this.settings = settings; 22 | } 23 | 24 | protected override void OnStart(string[] args) 25 | { 26 | base.OnStart(args); 27 | 28 | startup = new SelfHostStartup(settings); 29 | 30 | var options = new StartOptions(); 31 | 32 | if (!string.IsNullOrWhiteSpace(settings.ServerFactory)) 33 | { 34 | options.ServerFactory = settings.ServerFactory; 35 | Log.Info(m => m("Using ServerFactory {0}", options.ServerFactory)); 36 | }; 37 | 38 | var urls = settings.Urls.ToArray(); 39 | if (urls.Any()) 40 | { 41 | options.Urls.AddRange(urls); 42 | } 43 | else 44 | { 45 | options.Port = settings.Port; 46 | urls = new[] {"http://*:" + options.Port + "/"}; 47 | } 48 | 49 | server = WebApp.Start(options, startup.Configuration); 50 | 51 | Log.Info(m => m("Listening for HTTP requests on address(es): {0}", string.Join(", ", urls))); 52 | } 53 | 54 | protected override void OnStop() 55 | { 56 | Log.Info("Stopping HTTP server."); 57 | server.Dispose(); 58 | Log.Info("Waiting for background tasks to complete."); 59 | while (!startup.WaitForShutdown(TimeSpan.FromSeconds(1))) 60 | { 61 | RequestAdditionalTime(TimeSpan.FromSeconds(2)); 62 | } 63 | } 64 | 65 | protected virtual void RequestAdditionalTime(TimeSpan time) 66 | { 67 | if (interactive) return; 68 | 69 | RequestAdditionalTime((int)time.TotalMilliseconds); 70 | } 71 | 72 | public void RunInteractivley() 73 | { 74 | interactive = true; 75 | 76 | OnStart(new string[0]); 77 | 78 | Console.WriteLine("Press to stop."); 79 | Console.ReadLine(); 80 | 81 | OnStop(); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /* global require, module */ 2 | var EmberApp = require('ember-cli/lib/broccoli/ember-app'); 3 | var msbuild = require('broccoli-msbuild'); 4 | var select = require('broccoli-select'); 5 | 6 | module.exports = function(defaults) { 7 | var app = new EmberApp(defaults, { 8 | }); 9 | 10 | app.import('bower_components/jcaret/jquery.caret.js'); 11 | app.import('bower_components/momentjs/moment.js'); 12 | app.import('bower_components/nprogress/nprogress.js'); 13 | app.import('bower_components/nprogress/nprogress.css'); 14 | 15 | app.import({ 16 | development: 'bower_components/jquery-signalr/jquery.signalR.js', 17 | production: 'bower_components/jquery-signalr/jquery.signalR.min.js' 18 | }); 19 | 20 | function assetTree() { 21 | return select('bower_components', { 22 | acceptFiles: [ 23 | 'font-awesome/fonts/*' 24 | ], 25 | outputDir: '/assets' 26 | }); 27 | } 28 | 29 | function msbuildTree() { 30 | var msbuildInputTree = select('src', { 31 | acceptFiles: [ '**/*.csproj', '**/*.cs', '**/*.config' ], 32 | outputDir: '/build' 33 | }); 34 | 35 | var versionParts = app.project.pkg.version.split('-'); 36 | var versionPrefix = versionParts[0]; 37 | var versionSuffix = versionParts.length > 1 ? versionParts[1] : ''; 38 | 39 | var config = require('./config/environment')(app.env); 40 | 41 | if (config.disableMSBuild) { 42 | return null; 43 | } 44 | 45 | return msbuild(msbuildInputTree, { 46 | project: require('path').join(__dirname, 'Ciao.proj'), 47 | toolsVersion: '4.0', 48 | configuration: config.configuration, 49 | properties: { 50 | VersionPrefix: versionPrefix, 51 | VersionSuffix: versionSuffix, 52 | DistDir: '{destDir}' 53 | } 54 | }); 55 | } 56 | 57 | function buildTrees() { 58 | var trees = [assetTree()]; 59 | var msbuild = msbuildTree(); 60 | if (msbuild !== null) { 61 | trees.push(msbuild); 62 | } 63 | return trees; 64 | } 65 | 66 | // Use `app.import` to add additional libraries to the generated 67 | // output files. 68 | // 69 | // If you need to use different assets in different 70 | // environments, specify an object as the first parameter. That 71 | // object's keys should be the environment name and the values 72 | // should be the asset to use in that environment. 73 | // 74 | // If the library that you are including contains AMD or ES6 75 | // modules that you would like to import into your application 76 | // please specify an object with the list of modules as keys 77 | // along with the exports of each module as its value. 78 | 79 | return app.toTree(buildTrees()); 80 | }; 81 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost.Tests/CommandLineSettingsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | 4 | namespace Klondike.SelfHost.Tests 5 | { 6 | [TestFixture] 7 | public class CommandLineSettingsTests 8 | { 9 | [Test] 10 | public void KeyEqualsValueDoubleHyphen() 11 | { 12 | var settings = CommandLineSettings.Parse(new[] {"--somekey=true"}); 13 | 14 | Assert.That(settings.Get("somekey"), Is.True, "somekey"); 15 | } 16 | 17 | [Test] 18 | public void KeyEqualsValueSingleHyphen() 19 | { 20 | var settings = CommandLineSettings.Parse(new[] { "-x=5" }); 21 | 22 | Assert.That(settings.Get("x"), Is.EqualTo(5), "x"); 23 | } 24 | 25 | [Test] 26 | public void KeyEqualsValueSlash() 27 | { 28 | var settings = CommandLineSettings.Parse(new[] { "/x=five" }); 29 | 30 | Assert.That(settings.Get("x"), Is.EqualTo("five"), "x"); 31 | } 32 | 33 | [Test] 34 | public void CaseInsensitive() 35 | { 36 | var settings = CommandLineSettings.Parse(new[] { "/STUFF=five" }); 37 | 38 | Assert.That(settings.Get("stuff"), Is.EqualTo("five"), "stuff"); 39 | } 40 | 41 | [Test] 42 | public void ImplicitTrue() 43 | { 44 | var settings = CommandLineSettings.Parse(new[] { "--stuff" }); 45 | 46 | Assert.That(settings.Get("stuff"), Is.EqualTo(true), "x"); 47 | } 48 | 49 | [Test] 50 | public void GetThrowsOnMissingKey() 51 | { 52 | var settings = CommandLineSettings.Parse(new string[0]); 53 | 54 | TestDelegate call = () => settings.Get("stuff"); 55 | 56 | Assert.That(call, Throws.InstanceOf()); 57 | } 58 | 59 | [Test] 60 | public void ValueOrDefaultGetsValue() 61 | { 62 | var settings = CommandLineSettings.Parse(new[] { "--stuff" }); 63 | 64 | Assert.That(settings.GetValueOrDefault("stuff", false), Is.EqualTo(true), "x"); 65 | } 66 | 67 | [Test] 68 | public void ValueOrDefaultGetsDefault() 69 | { 70 | var settings = CommandLineSettings.Parse(new string[0]); 71 | 72 | Assert.That(settings.GetValueOrDefault("stuff", 157), Is.EqualTo(157), "stuff"); 73 | } 74 | 75 | [Test] 76 | public void MultipleValuesForKey() 77 | { 78 | var settings = CommandLineSettings.Parse(new[] {"--thing=a", "--thing=b"}); 79 | 80 | Assert.That(settings.GetValues("thing"), Is.EqualTo(new[] {"a", "b"})); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Klondike.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 2013 3 | VisualStudioVersion = 12.0.101101.0 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Klondike.WebHost", "src\Klondike.WebHost\Klondike.WebHost.csproj", "{07760DFA-72C3-45CD-8FC0-AA8D3274FCA5}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{6D8C35E9-01DB-49E3-973C-E7F9107BBA72}" 8 | ProjectSection(SolutionItems) = preProject 9 | .nuget\packages.config = .nuget\packages.config 10 | EndProjectSection 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Klondike.SelfHost", "src\Klondike.SelfHost\Klondike.SelfHost.csproj", "{1D086F30-D81E-4659-AC99-4CC602EDB3EA}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Ciao", "Ciao", "{62F71436-48EF-492E-BFFE-369497B7498B}" 15 | ProjectSection(SolutionItems) = preProject 16 | Ciao.proj = Ciao.proj 17 | Ciao.props = Ciao.props 18 | Ciao.targets = Ciao.targets 19 | EndProjectSection 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A120D6C6-71E5-439B-B01E-2908551335D4}" 22 | ProjectSection(SolutionItems) = preProject 23 | .editorconfig = .editorconfig 24 | appveyor.yml = appveyor.yml 25 | bower.json = bower.json 26 | Brocfile.js = Brocfile.js 27 | LICENSE.txt = LICENSE.txt 28 | NuGet.config = NuGet.config 29 | package.json = package.json 30 | README.md = README.md 31 | EndProjectSection 32 | EndProject 33 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Klondike.SelfHost.Tests", "src\Klondike.SelfHost.Tests\Klondike.SelfHost.Tests.csproj", "{81CC873B-029B-49F7-9757-008839C267F4}" 34 | EndProject 35 | Global 36 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 37 | Debug|Any CPU = Debug|Any CPU 38 | Release|Any CPU = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 41 | {07760DFA-72C3-45CD-8FC0-AA8D3274FCA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {07760DFA-72C3-45CD-8FC0-AA8D3274FCA5}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {07760DFA-72C3-45CD-8FC0-AA8D3274FCA5}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {07760DFA-72C3-45CD-8FC0-AA8D3274FCA5}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {1D086F30-D81E-4659-AC99-4CC602EDB3EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {1D086F30-D81E-4659-AC99-4CC602EDB3EA}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {1D086F30-D81E-4659-AC99-4CC602EDB3EA}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {1D086F30-D81E-4659-AC99-4CC602EDB3EA}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {81CC873B-029B-49F7-9757-008839C267F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {81CC873B-029B-49F7-9757-008839C267F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {81CC873B-029B-49F7-9757-008839C267F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {81CC873B-029B-49F7-9757-008839C267F4}.Release|Any CPU.Build.0 = Release|Any CPU 53 | EndGlobalSection 54 | GlobalSection(SolutionProperties) = preSolution 55 | HideSolutionNode = FALSE 56 | EndGlobalSection 57 | EndGlobal 58 | -------------------------------------------------------------------------------- /app/adapters/package.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Package from '../models/package'; 3 | import SearchResults from '../models/search-results'; 4 | import describePromise from 'klondike/util/describe-promise'; 5 | 6 | export default Ember.Object.extend({ 7 | defaultPageSize: 10, 8 | 9 | find: function (packageId, packageVersion) { 10 | var self = this; 11 | return this.get('restClient').ajax('packages.getPackageInfo', { 12 | data: { 13 | id: packageId, 14 | version: packageVersion 15 | } 16 | }).then(function(json) { 17 | return self._createPackageModel(json); 18 | }, null, describePromise(this, 'find', arguments)); 19 | }, 20 | 21 | search: function (query, page, pageSize, sortBy, sortOrder, includePrerelease, originFilter, latestOnly) { 22 | page = page || 0; 23 | pageSize = pageSize || this.get('defaultPageSize'); 24 | if (includePrerelease === undefined || includePrerelease === null) { 25 | includePrerelease = false; 26 | } 27 | if (latestOnly === undefined || latestOnly === null) { 28 | latestOnly = true; 29 | } 30 | 31 | var self = this; 32 | 33 | return this.get('restClient').ajax('packages.search', { 34 | data: { 35 | query: query, 36 | latestOnly: latestOnly, 37 | offset: page * pageSize, 38 | count: pageSize, 39 | sort: sortBy || 'score', 40 | order: sortOrder || 'ascending', 41 | includePrerelease: includePrerelease, 42 | originFilter: originFilter || 'any' 43 | } 44 | }).then(function(json) { 45 | if (json.query === null) { 46 | json.query = ''; 47 | } 48 | 49 | var hits = json.hits || []; 50 | hits = hits.map(function(hit) { return self._createPackageModel(hit); }); 51 | delete json.hits; 52 | 53 | return SearchResults.create(json, { 54 | hits: hits, 55 | page: page, 56 | pageSize: pageSize 57 | }); 58 | }, null, describePromise(this, 'search', arguments)); 59 | }, 60 | 61 | getAvailableSearchFields: function() { 62 | var result = this.get('_availableSearchFieldNames'); 63 | if (!result) { 64 | result = this.get('restClient').ajax('packages.getAvailableSearchFieldNames'); 65 | this.set('_availableSearchFieldNames', result); 66 | } 67 | return result; 68 | }, 69 | 70 | _createPackageModel: function(json) { 71 | var versions = json.versionHistory || []; 72 | var thisVersion = json.version; 73 | 74 | versions = versions.map(function(v) { 75 | return Package.create(v, { 76 | active: v.version === thisVersion 77 | }); 78 | }); 79 | 80 | versions = versions.sortBy('semanticVersion').reverse(); 81 | 82 | var tags = json.tags || []; 83 | if (typeof tags === 'string') { 84 | tags = tags.split(' ') 85 | .map(function(tag) { return tag.trim(); }) 86 | .filter(function(tag) { return tag !== ''; }); 87 | } 88 | 89 | return Package.create(json, { 90 | versionHistory: versions, 91 | tags: tags 92 | }); 93 | } 94 | }); 95 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | var childProcess = require('child_process') 2 | var httpProxy = require('http-proxy') 3 | var path = require('path') 4 | var Promise = require('RSVP').Promise; 5 | 6 | var port = 4201; 7 | var proc = null; 8 | var proxyServer = null; 9 | var proxyReady; 10 | 11 | // TODO: shadow copy on win32 12 | function start(virtualPathRoot) { 13 | var binDir = path.join(__dirname, '..', 'dist', 'bin'); 14 | var exe = path.join(binDir, 'Klondike.SelfHost.exe'); 15 | var args = []; 16 | var useMono = process.platform !== 'win32'; 17 | 18 | if (useMono) { 19 | args.push(exe); 20 | exe = 'mono'; 21 | } 22 | 23 | args.push('--interactive'); 24 | args.push('--port=' + port); 25 | args.push('--virtualPathRoot=' + virtualPathRoot); 26 | args.push('--packagesPath=' + path.normalize(path.join(__dirname, '..', 'packages'))); 27 | // TODO: make a real broccoli tree with proper cleanup: 28 | args.push('--lucenePath=' + path.normalize(path.join(__dirname, '..', 'tmp', 'lucene'))); 29 | args.push('--synchronizeOnStart'); 30 | 31 | proxyReady = new Promise(function(resolve, reject) { 32 | var doStart = function() { 33 | proc = childProcess.spawn(exe, args, { cwd: binDir }); 34 | 35 | proc.stdout.on('data', function(data) { 36 | if (data.toString().match(/Listening for HTTP requests/i)) { 37 | proxyServer = new httpProxy.createProxyServer({ 38 | target: { 39 | host: 'localhost', 40 | port: port 41 | } 42 | }); 43 | 44 | resolve(proxyServer); 45 | } 46 | }); 47 | 48 | proc.stderr.on('data', function(data) { 49 | console.error('' + data); 50 | }); 51 | 52 | } 53 | 54 | if (proc != null) { 55 | proc.on('exit', doStart); 56 | proc.stdin.write('quit\n'); 57 | proc = null; 58 | proxyServer = null; 59 | } else { 60 | doStart(); 61 | } 62 | }); 63 | } 64 | 65 | module.exports = function(app, options) { 66 | if (options.environment === 'ember-only') { 67 | return; 68 | } 69 | 70 | if (!options || !options.watcher) { 71 | console.warn('Watcher is not available. Not proxying requests to Klondike.SelfHost.'); 72 | return; 73 | } 74 | 75 | var baseURL = options.baseURL; 76 | if (baseURL[baseURL.length-1] !== '/') { 77 | baseURL += '/'; 78 | } 79 | 80 | options.watcher.on('change', function() { start(baseURL); }); 81 | 82 | app.all(baseURL + 'api/*', function(req, res, next) { 83 | if (proxyReady == null) { 84 | res.status(502).send('Bad Gateway'); 85 | return; 86 | } 87 | 88 | proxyReady.then(function() { 89 | try { 90 | proxyServer.web(req, res); 91 | } catch (err) { 92 | console.error('HTTP Proxy to Klondike.SelfHost failed:', err); 93 | res.status(502).send('Bad Gateway'); 94 | } 95 | }); 96 | }); 97 | 98 | app.on('upgrade', function (req, socket, head) { 99 | if (proxyReady == null) { 100 | res.status(502).send('Bad Gateway'); 101 | return; 102 | } 103 | 104 | proxyReady.then(function() { 105 | try { 106 | proxyServer.ws(req, socket, head); 107 | } catch (err) { 108 | console.error('WebSocket Proxy to Klondike.SelfHost failed:', err); 109 | res.status(502).send('Bad Gateway'); 110 | } 111 | }); 112 | }); 113 | } 114 | -------------------------------------------------------------------------------- /app/controllers/users/edit.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import BaseControllerMixin from 'klondike/mixins/base-controller'; 3 | import UserPermissionObserver from 'klondike/mixins/user-permission-observer'; 4 | import ProgressIndicator from 'klondike/progress-indicator'; 5 | 6 | export default Ember.Controller.extend(BaseControllerMixin, UserPermissionObserver, { 7 | errorMessage: '', 8 | originalUsername: '', 9 | actionLabel: 'Edit', 10 | isAllowedToDelete: false, 11 | isSaving: false, 12 | isSaveCompleted: false, 13 | 14 | modelDidChange: Ember.observer('model',function() { 15 | this.set('originalUsername', this.get('model.username')); 16 | this.set('isSaving', false); 17 | this.set('isSaveCompleted', false); 18 | }), 19 | 20 | init: function() { 21 | this._super(); 22 | this.observeUserPermission('isAllowedToDelete', 'users.delete'); 23 | }, 24 | 25 | canDelete: function() { 26 | return this.get('isAllowedToDelete'); 27 | }.property('isAllowedToDelete'), 28 | 29 | isRenamed: function() { 30 | if (Ember.isEmpty(this.get('originalUsername'))) { 31 | return false; 32 | } 33 | return this.get('originalUsername') !== this.get('model.username'); 34 | }.property('originalUsername', 'model.username'), 35 | 36 | reset: function() { 37 | this.set('errorMessage', ''); 38 | this.set('isSaving', false); 39 | this.set('isSaveCompleted', false); 40 | }, 41 | 42 | actions: { 43 | save: function () { 44 | this.set('errorMessage', ''); 45 | 46 | var user = { 47 | username: this.get('model.username'), 48 | key: this.get('model.key'), 49 | roles: this.get('model.roles') 50 | }; 51 | 52 | if (this.get('isRenamed')) { 53 | user.renameTo = user.username; 54 | user.username = this.get('originalUsername'); 55 | user.overwrite = false; 56 | } 57 | 58 | var promise = this.get('users').update(user); 59 | 60 | return this._wrapAjaxPromise(promise); 61 | }, 62 | delete: function() { 63 | this.set('errorMessage', ''); 64 | 65 | var promise = this.get('users').delete(this.get('originalUsername')); 66 | return this._wrapAjaxPromise(promise); 67 | }, 68 | cancel: function() { 69 | this.transitionToRoute('users.list'); 70 | } 71 | }, 72 | 73 | _wrapAjaxPromise: function(promise) { 74 | ProgressIndicator.start(); 75 | this.set('isSaving', true); 76 | 77 | var self = this; 78 | 79 | var finish = function() { 80 | self.set('isSaving', false); 81 | ProgressIndicator.done(); 82 | }; 83 | 84 | return promise.then(function() { 85 | finish(); 86 | self.set('isSaveCompleted', true); 87 | self.transitionToRoute('users.list'); 88 | }).catch(function(err) { 89 | finish(); 90 | var message = 'Unknown error occurred.'; 91 | 92 | if (err.response && err.response.message) { 93 | message = err.response.message; 94 | } 95 | 96 | self.set('errorMessage', message); 97 | }); 98 | } 99 | }); 100 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost.Tests/SelfHostVirtualPathUtilityTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using System.IO; 4 | 5 | namespace Klondike.SelfHost.Tests 6 | { 7 | [TestFixture] 8 | public class SelfHostVirtualPathUtilityTests 9 | { 10 | [Test] 11 | [TestCase(null)] 12 | [TestCase("")] 13 | [TestCase("not_rooted")] 14 | public void Ctr_WebRoot_ArgumentException(string webRoot) 15 | { 16 | TestDelegate call = () => new SelfHostVirtualPathUtility(webRoot, ""); 17 | 18 | Assert.That(call, Throws.ArgumentException); 19 | } 20 | 21 | [Test] 22 | [TestCase("not_rooted")] 23 | public void Ctr_VirtualPathRoot_ArgumentException(string virtualPathRoot) 24 | { 25 | TestDelegate call = () => new SelfHostVirtualPathUtility(Environment.CurrentDirectory, virtualPathRoot); 26 | 27 | Assert.That(call, Throws.ArgumentException); 28 | } 29 | 30 | public void Ctr_WebRoot_ArgumentException_DoesNotExist() 31 | { 32 | Ctr_WebRoot_ArgumentException(Path.Combine(Environment.CurrentDirectory, "NoSuchSubFolder")); 33 | } 34 | 35 | [Test] 36 | [TestCase("~/foo")] 37 | [TestCase("~//foo")] 38 | [TestCase("foo")] 39 | public void MapPath(string virtualPath) 40 | { 41 | Assert.That(Utility.MapPath(virtualPath), Is.EqualTo(Path.Combine(Environment.CurrentDirectory, "foo"))); 42 | } 43 | 44 | [Test] 45 | public void MapPath_Absolute() 46 | { 47 | var absolutePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); 48 | 49 | Assert.That(Utility.MapPath(absolutePath), Is.EqualTo(absolutePath)); 50 | } 51 | 52 | [Test] 53 | public void ToAbsolute() 54 | { 55 | var result = Utility.ToAbsolute(Path.Combine(Environment.CurrentDirectory, "js", "foo.js")); 56 | 57 | Assert.That(result, Is.EqualTo("/js/foo.js")); 58 | } 59 | 60 | [Test] 61 | public void ToAbsolute_HandlesTrailingSlashOnWebRoot() 62 | { 63 | var utility = new SelfHostVirtualPathUtility(Environment.CurrentDirectory + Path.DirectorySeparatorChar, ""); 64 | 65 | var result = utility.ToAbsolute("foo.js"); 66 | 67 | Assert.That(result, Is.EqualTo("/foo.js")); 68 | } 69 | 70 | [Test] 71 | public void ToAbsolute_NestedVirtualPathRoot() 72 | { 73 | var utility = new SelfHostVirtualPathUtility(Environment.CurrentDirectory, "/child-app"); 74 | 75 | var result = utility.ToAbsolute("~/js/foo.js"); 76 | 77 | Assert.That(result, Is.EqualTo("/child-app/js/foo.js")); 78 | } 79 | 80 | [Test] 81 | public void ToAbsolute_NestedVirtualPathRoot_TrailingSlash() 82 | { 83 | var utility = new SelfHostVirtualPathUtility(Environment.CurrentDirectory, "/child/app/"); 84 | 85 | var result = utility.ToAbsolute("~/js/foo.js"); 86 | 87 | Assert.That(result, Is.EqualTo("/child/app/js/foo.js")); 88 | } 89 | 90 | [Test] 91 | public void ToAbsolute_HandlesSlashOnVirtualPathRoot() 92 | { 93 | var utility = new SelfHostVirtualPathUtility(Environment.CurrentDirectory, "/"); 94 | 95 | var result = utility.ToAbsolute("~/js/foo.js"); 96 | 97 | Assert.That(result, Is.EqualTo("/js/foo.js")); 98 | } 99 | 100 | public SelfHostVirtualPathUtility Utility 101 | { 102 | get { return new SelfHostVirtualPathUtility(Environment.CurrentDirectory, ""); } 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost.Tests/Klondike.SelfHost.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {81CC873B-029B-49F7-9757-008839C267F4} 8 | Library 9 | Properties 10 | Klondike.SelfHost.Tests 11 | Klondike.SelfHost.Tests 12 | v4.5 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | ..\..\build\Klondike.SelfHost.Tests\bin\Debug\ 21 | ..\..\build\Klondike.SelfHost.Tests\obj\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | false 26 | 27 | 28 | pdbonly 29 | true 30 | ..\..\build\Klondike.SelfHost.Tests\bin\Release\ 31 | ..\..\build\Klondike.SelfHost.Tests\obj\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | false 36 | 37 | 38 | 39 | False 40 | ..\..\packages\NUnit.2.6.4\lib\nunit.framework.dll 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {1d086f30-d81e-4659-ac99-4cc602edb3ea} 61 | Klondike.SelfHost 62 | 63 | 64 | {07760DFA-72C3-45CD-8FC0-AA8D3274FCA5} 65 | Klondike.WebHost 66 | 67 | 68 | 69 | 76 | -------------------------------------------------------------------------------- /src/Klondike.WebHost/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost/SelfHostSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using NuGet.Lucene.Web; 6 | 7 | namespace Klondike.SelfHost 8 | { 9 | class SelfHostSettings : NuGetWebApiSettings 10 | { 11 | private readonly CommandLineSettings commandLineSettings; 12 | private readonly IVirtualPathUtility virtualPathUtility; 13 | 14 | public SelfHostSettings(CommandLineSettings commandLineSettings) 15 | :base(prefix:"") 16 | { 17 | this.commandLineSettings = commandLineSettings; 18 | this.virtualPathUtility = new SelfHostVirtualPathUtility(BaseDirectory, VirtualPathRoot); 19 | } 20 | 21 | protected override string GetAppSetting(string key, string defaultValue) 22 | { 23 | if (string.Equals(key, "routePathPrefix", StringComparison.InvariantCultureIgnoreCase)) 24 | { 25 | defaultValue = (VirtualPathRoot.TrimEnd('/') + '/' + defaultValue).TrimStart('/'); 26 | } 27 | return commandLineSettings.GetValueOrDefault(key, base.GetAppSetting(key, defaultValue)); 28 | } 29 | 30 | protected override string MapPathFromAppSetting(string key, string defaultValue) 31 | { 32 | return MapPath(GetAppSetting(key, defaultValue)); 33 | } 34 | 35 | public int Port 36 | { 37 | get { return Convert.ToInt32(GetAppSetting("port", "8080")); } 38 | } 39 | 40 | public IEnumerable Urls 41 | { 42 | get 43 | { 44 | var urls = commandLineSettings.GetValues("url").ToArray(); 45 | if (urls.Any()) 46 | { 47 | return urls; 48 | } 49 | 50 | return base.GetAppSetting("url", "").Split(',').Select(s => s.Trim()).Where(s => !string.IsNullOrWhiteSpace(s)); 51 | } 52 | } 53 | 54 | public string ServerFactory 55 | { 56 | get { return GetAppSetting("serverFactory", "Nowin"); } 57 | } 58 | 59 | public string BaseDirectory 60 | { 61 | get { return GetAppSetting("baseDirectory", DefaultBaseDirectory); } 62 | } 63 | 64 | public string VirtualPathRoot 65 | { 66 | get { return GetAppSetting("virtualPathRoot", "/"); } 67 | } 68 | 69 | public IVirtualPathUtility VirtualPathUtility 70 | { 71 | get { return virtualPathUtility; } 72 | } 73 | 74 | public bool EnableAnonymousAuthentication 75 | { 76 | get { return GetFlagFromAppSetting("enableAnonymousAuthentication", true); } 77 | } 78 | 79 | public bool EnableIntegratedWindowsAuthentication 80 | { 81 | get { return GetFlagFromAppSetting("enableIntegratedWindowsAuthentication", false); } 82 | } 83 | 84 | public bool Interactive 85 | { 86 | get { return GetFlagFromAppSetting("interactive", false); } 87 | } 88 | 89 | public string MapPath(string path) 90 | { 91 | return virtualPathUtility.MapPath(path); 92 | } 93 | 94 | private static readonly string DefaultBaseDirectory = ResolveBaseDirectory(); 95 | 96 | private static string ResolveBaseDirectory() 97 | { 98 | var binPath = AppDomain.CurrentDomain.SetupInformation.ApplicationBase ?? Directory.GetCurrentDirectory(); 99 | var index = binPath.LastIndexOf(Path.DirectorySeparatorChar + "bin", StringComparison.InvariantCultureIgnoreCase); 100 | if (index > 0) 101 | { 102 | binPath = binPath.Substring(0, index); 103 | } 104 | 105 | return binPath; 106 | } 107 | 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost/SelfHostStartup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Web.Http; 4 | using Autofac; 5 | using Microsoft.Owin; 6 | using Microsoft.Owin.FileSystems; 7 | using Microsoft.Owin.StaticFiles; 8 | using NuGet.Lucene.Web; 9 | using Owin; 10 | 11 | namespace Klondike.SelfHost 12 | { 13 | class SelfHostStartup : Klondike.Startup 14 | { 15 | private readonly SelfHostSettings selfHostSettings; 16 | 17 | public SelfHostStartup(SelfHostSettings selfHostSettings) 18 | { 19 | this.selfHostSettings = selfHostSettings; 20 | } 21 | 22 | protected override IContainer CreateContainer(IAppBuilder app) 23 | { 24 | var builder = new ContainerBuilder(); 25 | builder.RegisterInstance(selfHostSettings.VirtualPathUtility).As(); 26 | var container = base.CreateContainer(app); 27 | builder.Update(container); 28 | return container; 29 | } 30 | 31 | protected override INuGetWebApiSettings CreateSettings() 32 | { 33 | return selfHostSettings; 34 | } 35 | 36 | protected override HttpConfiguration CreateHttpConfiguration() 37 | { 38 | return new HttpConfiguration(); 39 | } 40 | 41 | protected override void Start(IAppBuilder app, IContainer container) 42 | { 43 | ConfigureAuthentication(app); 44 | 45 | base.Start(app, container); 46 | 47 | var fileServerOptions = new FileServerOptions 48 | { 49 | FileSystem = new PhysicalFileSystem(selfHostSettings.BaseDirectory), 50 | RequestPath = new PathString(selfHostSettings.VirtualPathRoot.TrimEnd('/')), 51 | EnableDefaultFiles = true 52 | }; 53 | fileServerOptions.DefaultFilesOptions.DefaultFileNames = new[] {"index.html"}; 54 | app.UseFileServer(fileServerOptions); 55 | } 56 | 57 | private void ConfigureAuthentication(IAppBuilder app) 58 | { 59 | if (selfHostSettings.EnableAnonymousAuthentication && !selfHostSettings.EnableIntegratedWindowsAuthentication) 60 | { 61 | return; 62 | } 63 | 64 | var schemes = AuthenticationSchemes.None; 65 | 66 | if (selfHostSettings.EnableAnonymousAuthentication) 67 | { 68 | schemes |= AuthenticationSchemes.Anonymous; 69 | } 70 | 71 | if (selfHostSettings.EnableIntegratedWindowsAuthentication) 72 | { 73 | schemes |= AuthenticationSchemes.IntegratedWindowsAuthentication; 74 | } 75 | 76 | object listenerObj; 77 | if (!app.Properties.TryGetValue("System.Net.HttpListener", out listenerObj)) 78 | { 79 | throw new InvalidOperationException("Integrated Windows Authentication can only be enabled when using Microsoft.Owin.Host.HttpListener Server Factory."); 80 | } 81 | 82 | var listener = (HttpListener) listenerObj; 83 | 84 | var mixed = selfHostSettings.EnableAnonymousAuthentication && 85 | selfHostSettings.EnableIntegratedWindowsAuthentication; 86 | 87 | if (mixed) 88 | { 89 | listener.AuthenticationSchemeSelectorDelegate = req => 90 | { 91 | var path = req.Url.GetComponents(UriComponents.Path, UriFormat.UriEscaped); 92 | if (path.EndsWith("api/authenticate")) 93 | { 94 | return AuthenticationSchemes.IntegratedWindowsAuthentication; 95 | } 96 | return AuthenticationSchemes.Anonymous; 97 | }; 98 | } 99 | 100 | listener.AuthenticationSchemes = schemes; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/templates/packages/search.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{drop-down selectedValue=originFilter content=originFilters classNames='field'}} 4 | {{drop-down selectedValue=sortBy content=sortByColumns classNames='field'}} 5 | {{drop-down selectedValue=includePrerelease content=versionFilters classNames='field'}} 6 | {{drop-down selectedValue=latestOnly content=latestOnlyFilters classNames='field'}} 7 | 8 |
9 | {{#link-to 'packages.advanced-search' class="btn button-link kl-button--link"}}Advanced search{{/link-to}} 10 |
11 |
12 |
13 | 14 |
15 |

16 | {{#if query}} 17 | Search results for "{{query}}" 18 | {{else}} 19 | List of All Packages 20 | {{/if}} 21 |

22 | 23 | {{#if total}} 24 |
25 | 26 | 27 | 28 | 29 | 30 | Previous Page 31 | 32 | 33 | Next Page 34 | 35 | 36 | 37 | 38 | 39 | Showing {{first}} to {{last}} of {{total}} 40 |
41 | 42 |
    43 | {{#each model.hits as |hit|}} 44 |
  1. 45 |
    46 |
    47 | {{#link-to 'packages.view' hit classNames="inline-block bold h3"}} 48 | {{#if hit.title}} 49 | {{hit.title}} 50 | {{else}} 51 | {{hit.id}} 52 | {{/if}} 53 | {{/link-to}} 54 | {{hit.version}} 55 | 56 |
    57 | {{hit.description}} 58 |
    59 | {{search-link-list header='Authors' items=hit.authors}} 60 | {{search-link-list header='Tags' items=hit.tags}} 61 |
    62 |
    63 | Downloads: 64 | 65 | {{#if hit.downloadCount}} 66 | {{hit.downloadCount}} 67 | {{#unless latestOnly}} 68 | total, 69 | {{hit.versionDownloadCount}} of this version 70 | {{/unless}} 71 | {{else}} 72 | 0 73 | {{/if}} 74 | 75 |
    76 |
    77 |
  2. 78 | {{/each}} 79 |
80 | 81 | 97 | {{else}} 98 | No results :'( 99 | {{/if}} 100 |
101 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /Ciao.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $([System.IO.Path]::Combine('$(ProjectDirectory)', 'src', 'Klondike.WebHost'))$([System.IO.Path]::DirectorySeparatorChar) 5 | $([System.IO.Path]::Combine('$(ProjectDirectory)', 'build', 'Klondike.WebHost'))$([System.IO.Path]::DirectorySeparatorChar) 6 | 7 | $([System.IO.Path]::Combine('$(ProjectDirectory)', 'src', 'Klondike.SelfHost'))$([System.IO.Path]::DirectorySeparatorChar) 8 | $([System.IO.Path]::Combine('$(ProjectDirectory)', 'build', 'Klondike.SelfHost'))$([System.IO.Path]::DirectorySeparatorChar) 9 | $([System.IO.Path]::Combine('$(ProjectDirectory)', '$(DistDir)'))$([System.IO.Path]::DirectorySeparatorChar) 10 | Debug 11 | 12 | 13 | 14 | $(SolutionDirectory)packages\WebConfigTransformRunner.1.1.16\Tools\WebConfigTransformRunner.exe 15 | "$(TransformExe)" 16 | mono --runtime=v4.0.30319 $(TransformExe) 17 | 18 | 19 | 20 | 21 | $(BuildDependsOn); 22 | CopyOutputsToDistDir; 23 | TransformWebConfig; 24 | TransformSelfHostConfig 25 | 26 | 27 | $(TestDependsOn); 28 | IntegrationTest 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 48 | 49 | 51 | 52 | 53 | 57 | 58 | 61 | 62 | 65 | 66 | 67 | 71 | 72 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/session.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import describePromise from 'klondike/util/describe-promise'; 3 | 4 | function promiseAlias(name) { 5 | return function () { 6 | var promise = this.get('_promise'); 7 | return promise[name].apply(promise, arguments); 8 | }; 9 | } 10 | 11 | export default Ember.Object.extend({ 12 | users: null, 13 | fixedKeyBinding: 'application.apiKey', 14 | 15 | user: null, 16 | usernameBinding: 'user.username', 17 | keyBinding: 'user.key', 18 | rolesBinding: 'user.roles', 19 | isInitialized: false, 20 | 21 | _promise: null, 22 | 23 | isLoggedIn: Ember.computed('username', function () { 24 | return !Ember.isEmpty(this.get('username')); 25 | }), 26 | 27 | init: function () { 28 | this._super(); 29 | 30 | var self = this; 31 | var settings = {}; 32 | var sessionKey = window.sessionStorage.getItem('key') || this.get('fixedKey'); 33 | 34 | if (!Ember.isEmpty(sessionKey)) { 35 | settings.beforeSend = function(xhr) { 36 | xhr.setRequestHeader(self.get('restClient').get('apiKeyRequestHeaderName'), sessionKey); 37 | }; 38 | } 39 | 40 | var initPromise = self._invokeLogin('users.getAuthenticationInfo', settings).then(function() { 41 | self.set('isInitialized', true); 42 | return true; 43 | }, function() { 44 | self.set('isInitialized', true); 45 | return true; 46 | }, describePromise(this, 'init')); 47 | 48 | self.set('_promise', initPromise); 49 | }, 50 | 51 | 'then': promiseAlias('then'), 52 | 'catch': promiseAlias('catch'), 53 | 'finally': promiseAlias('finally'), 54 | 55 | isAllowed: function(apiName, method) { 56 | var self = this; 57 | 58 | return this.get('restClient').getApi(apiName, method).then(function (api) { 59 | if (!api.requiresAuthentication) { 60 | return true; 61 | } 62 | 63 | var userRoles = self.get('user.roles') || []; 64 | var userRoleMissing = function(roleName) { 65 | return !userRoles.contains(roleName); 66 | }; 67 | 68 | var any = api.requiresRoles.any(function(roleSet) { 69 | return roleSet.any(userRoleMissing); 70 | }); 71 | 72 | return !any; 73 | }, null, describePromise(this, 'isAllowed', arguments)); 74 | }, 75 | 76 | logOut: function() { 77 | this.set('user', null); 78 | }, 79 | 80 | tryLogIn: function() { 81 | return this.logIn().then(function() { 82 | return true; 83 | }).catch(function() { 84 | return false; 85 | }); 86 | }, 87 | 88 | logIn: function(username, password) { 89 | var settings = {}; 90 | 91 | if (!Ember.isEmpty(username)) { 92 | settings.beforeSend = function(xhr) { 93 | xhr.setRequestHeader('Authorization', 'Basic ' + window.btoa(username + ':' + password)); 94 | }; 95 | } 96 | 97 | return this._invokeLogin('users.getRequiredAuthenticationInfo', settings); 98 | }, 99 | 100 | changeKey: function() { 101 | var self = this; 102 | 103 | var settings = { 104 | type: 'POST', 105 | data: { key: '' } 106 | }; 107 | 108 | var call = this.get('restClient').ajax('users.changeApiKey', settings); 109 | 110 | return call.then(function(data) { 111 | self.set('key', data.key); 112 | window.sessionStorage.setItem('key', self.get('key')); 113 | return data.key; 114 | }, null, describePromise(this, 'changeKey')); 115 | }, 116 | 117 | _invokeLogin: function (apiName, settings) { 118 | var self = this; 119 | 120 | return self.get('restClient').ajax(apiName, settings).then(function(json) { 121 | json = json || {}; 122 | json.roles = Ember.A(json.roles || []); 123 | 124 | var user = self.get('store').createModel('user', json); 125 | 126 | self.set('user', user); 127 | 128 | return user; 129 | }, function(err) { 130 | self.set('user', null); 131 | throw err; 132 | }, describePromise(this, '_invokeLogin')); 133 | }, 134 | 135 | _keyDidChange: Ember.observer('key', function() { 136 | var key = this.get('key'); 137 | if (key) { 138 | window.sessionStorage.setItem('key', key); 139 | } else { 140 | window.sessionStorage.removeItem('key'); 141 | } 142 | this.get('restClient').set('apiKey', key); 143 | }), 144 | }); 145 | -------------------------------------------------------------------------------- /src/Klondike.SelfHost.Tests/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /app/templates/packages/view.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{package-icon url=model.iconUrl}} 4 |

{{model.displayTitle}}

5 | {{model.version}} 6 |
7 | 8 |
9 |
10 |

{{summary}}

11 | 12 | {{#if hasAdditionalDescription}} 13 |

{{model.description}}

14 | {{/if}} 15 | 16 |

To install {{id}} run this command in the Package Manager Console

17 | {{code-snippet content=installCommand prompt='PM> '}} 18 | 19 | {{search-link-list header='Authors' items=model.authors}} 20 | {{search-link-list header='Owners' items=model.owners}} 21 | 22 |

Links

23 |
    24 | {{#if model.projectUrl}} 25 |
  • Project Site
  • 26 | {{/if}} 27 | {{#if model.licenseUrl}} 28 |
  • License
  • 29 | {{/if}} 30 |
31 | 32 | {{#if model.releaseNotes}} 33 |
34 | Release Notes 35 |
36 | {{model.releaseNotes}} 37 |
38 |
39 | {{/if}} 40 | 41 | {{#if model.supportedFrameworks}} 42 |
43 | Supported Frameworks 44 |
    45 | {{#each model.supportedFrameworks as |framework|}} 46 |
  • {{framework}}
  • 47 | {{/each}} 48 |
49 |
50 | {{/if}} 51 | 52 | {{#if model.dependencySets}} 53 |
54 | Dependencies 55 | 56 | 57 | {{#each model.dependencySets as |set|}} 58 | 59 | 60 | 61 | {{#each set.dependencies as |dep|}} 62 | 63 | 66 | 67 | 68 | {{/each}} 69 | {{/each}} 70 | 71 |
{{set.targetFramework.fullName}}
64 | {{dep.id}} 65 | {{dep.versionSpec.minVersion}}
72 |
73 | {{/if}} 74 | 75 | {{#if model.files}} 76 |
77 | Package Contents 78 |
    79 | {{#each model.files as |file|}} 80 |
  • {{file}}
  • 81 | {{/each}} 82 |
83 |
84 | {{/if}} 85 | 86 |
87 | Version History 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | {{#each model.versionHistory as |item|}} 96 | 97 | 98 | 99 | 100 | 101 | {{/each}} 102 | 103 |
VersionDownloadsLast Update
{{#link-to 'packages.view' item}}{{item.version}}{{/link-to}}{{item.versionDownloadCount}}{{format-date item.lastUpdated}}
104 |
105 | 106 |
107 | 108 |
109 | {{#if model.copyright}} 110 |
{{model.copyright}}
111 | {{/if}} 112 | 113 | {{#if model.isMirrored}} 114 |
Mirrored from {{origin}}
115 | {{/if}} 116 | 117 |
118 |
119 | {{#if model.symbolsAvailable}} 120 | 121 | 122 | 123 | {{else}} 124 | 125 | 126 | 127 | {{/if}} 128 |
129 | Symbols and source code are 130 | {{#unless model.symbolsAvailable}}not{{/unless}} 131 | available for this package. 132 |
133 |
134 |
135 | 136 | {{search-link-list header='Tags' items=model.tags}} 137 |
138 |
139 |
140 | -------------------------------------------------------------------------------- /src/Klondike.WebHost/Settings.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 17 | 18 | 21 | 22 | 27 | 28 | 34 | 35 | 40 | 41 | 46 | 47 | 51 | 52 | 61 | 62 | 71 | 72 | 76 | 77 | 82 | 83 | 89 | 90 | 96 | 97 | 104 | 105 | 112 | 113 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Klondike [![Build status](https://ci.appveyor.com/api/projects/status/vxqnth8eyerocfpm/branch/master?svg=true)](https://ci.appveyor.com/project/chriseldredge/klondike/branch/master) 2 | 3 | Ember front-end that builds on NuGet.Lucene for private [NuGet](https://www.nuget.org/) package hosting. 4 | 5 | ## Binaries 6 | 7 | Available from the Releases tab on github. 8 | 9 | Alternatively, you can clone the [Klondike-Release](https://github.com/themotleyfool/Klondike-Release) 10 | git repo to make upgrading easier. 11 | 12 | Preview binaries can be obtained from the `Artifacts` tab of a successful [AppVeyor build](https://ci.appveyor.com/project/chriseldredge/klondike/). 13 | 14 | ## What is Klondike 15 | 16 | Klondike is an asp.net web application you deploy to your own web server or to the cloud 17 | that works as a private NuGet package feed for storing private packages your organization 18 | creates. Klondike can also automatically restore packages sourced from 3rd party feeds, 19 | such as the nuget.org public feed, to keep your build server humming even when nuget.org 20 | is unavailable. 21 | 22 | Klondike performs dramatically better than the standard NuGet.Server provider and adds lots 23 | of extra features you can't get anywhere else. Klondike uses Lucene.Net meaning that the 24 | install footprint is light. Simply grab the binaries, stand up an IIS site (or run the self-hosted 25 | exe) and you're done. Much easier than deploying your own NuGet Gallery. 26 | 27 | ## How to Deploy Klondike 28 | 29 | 1. Grab a binary zip from the Releases tab or clone 30 | [Klondike-Release](https://github.com/themotleyfool/Klondike-Release) 31 | 1. Customize [Settings.config](src/Klondike.WebHost/Settings.config) 32 | 1. Create a site in IIS using a .NET v4.0 Integrated Pipeline application pool 33 | 34 | _N.B._ Klondike works best deployed as a root application and is only supported in this configuration. 35 | There are known issues with NuGet clients when attempting to host Klondike as a child application on a 36 | virtual path. In addition, the Ember web application will not work correctly unless it is rebuilt 37 | with a different virtual path. 38 | 39 | ## App Pool Advanced Configuration 40 | 41 | Klondike is designed to run as a single process to avoid conflicting writes on 42 | the Lucene index files. Adjust your application pool accordingly: 43 | 44 | * Make sure `Maximum Worker Processes` is set to `1` 45 | * Make sure `Disable Overlapped Recycle` is set to `true` 46 | 47 | ## Authentication and Role-Based Security 48 | 49 | Klondike supports external authentication providers such as Windows (Active Directory), 50 | basic auth and NTLM. These are configured in IIS Manager and other tools. 51 | 52 | Disable anonymous authentication to require authentication even for read access to Klondike. 53 | 54 | In addition to standard authentication, Klondike supports authentication by using the 55 | `X-NuGet-ApiKey` HTTP Request header to be compatible with NuGet clients that push and delete 56 | packages. 57 | 58 | ### Local Administrator 59 | 60 | Browsing or accessing the Klondike app from a local network interface on the same machine 61 | will implicitly grant access as `LocalAdministrator`. This account is allowed to create 62 | additional users, push and delete packages. 63 | 64 | You can disable this behavior by editing `handleLocalRequestsAsAdmin` in [Settings.config](src/Klondike.WebHost/Settings.config). 65 | 66 | ### Mapping Active Directory Roles to Klondike 67 | 68 | Edit the `roleMappings` section in [Web.config](src/Klondike.WebHost/Web.config) to grant 69 | Klondike roles for user administration and package management to existing roles in your 70 | external security provider (such as Active Directory). Multiple groups can be specified 71 | delimited by commas. Membership in any one role is sufficient to grant a Klondike role. 72 | 73 | The available roles and their permissions are: 74 | 75 | * PackageManager - Allowed to push and delete packages 76 | * AccountAdministrator - Allowed to administer accounts 77 | 78 | ### Creating Users and Passwords 79 | 80 | Any user who has the AccountAdministrator role may create, delete and modify accounts 81 | and API keys. This includes the LocalAdministrator account. 82 | 83 | To access this feature, browse to Klondike and select `Admin` in the top navigation, 84 | then `Manage Accounts`. 85 | 86 | ## Self-Hosted Klondike 87 | 88 | The binary release also includes Klondike.SelfHost.exe in the bin directory. 89 | It can be run from the console using mono or the .net framework: 90 | 91 | Klondike.SelfHost.exe --port=8080 92 | 93 | Or 94 | 95 | mono ./Klondike.SelfHost.exe --interactive --port=8080 96 | 97 | Klondike requires Mono 4.2.0 or later. 98 | 99 | If no port is specified, 8080 is used as a default. See the [Klondike.SelfHost README](src/Klondike.SelfHost/README.md) 100 | for more information. 101 | 102 | ## Building Locally 103 | 104 | This repository consists of two components: 105 | 106 | 1. Ember front-end built and packaged by [ember-cli](http://www.ember-cli.com/) 107 | 1. c# project built by MSBuild or xbuild 108 | 109 | ### Front End 110 | 111 | Prerequisites: node (`node` and `npm` should be on your PATH). 112 | 113 | Install ember-cli and bower if you haven't already: 114 | 115 | npm install -g ember-cli@0.2.7 116 | 117 | Install dependencies: 118 | 119 | npm install && bower install 120 | 121 | Finally, build: 122 | 123 | ember build 124 | 125 | This puts the built app into `./dist`. 126 | 127 | _Note_: if you do not have the .NET 4.5 SDK or Mono 3.6 MDK installed you can 128 | skip building the .net assets by using the `ember-only` environment: 129 | 130 | ember build --environment=ember-only 131 | 132 | ### .NET Back End 133 | 134 | The c# projects can be built on Windows or OS X / Linux. On Windows, 135 | install Visual Studio 2013 and the Microsoft.NET Framework 4.5 SDK. 136 | On OS X / Linux, install the [Mono MDK](http://www.mono-project.com/download/) 137 | 138 | Mono can also be installed by [homebrew](http://brew.sh/) on OS X. 139 | 140 | ## Front End development without .NET 141 | 142 | You can develop the front end without needing to build or host the .net code. 143 | 144 | Edit [config/environment.js](config/environment.js) and set the `apiURL` 145 | and (optionally) `apiKey` properties to point to an external Klondike API endpoint, 146 | then run 147 | 148 | ember serve --environment=ember-only 149 | 150 | ## Previewing debug/release builds 151 | 152 | You can serve production builds with: 153 | 154 | ember serve --environment=production 155 | 156 | ## Integration Tests 157 | 158 | Coming Real Soon Now. 159 | -------------------------------------------------------------------------------- /Ciao.proj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | $(MSBuildThisFileDirectory) 13 | $([System.IO.Path]::Combine('$(MSBuildThisFileDirectory)', 'build'))$([System.IO.Path]::DirectorySeparatorChar) 14 | $([System.IO.Path]::Combine('$(BuildDirectory)', 'tools'))$([System.IO.Path]::DirectorySeparatorChar) 15 | $([System.IO.Path]::Combine('$(ToolsDirectory)', 'NuGet.exe')) 16 | 17 | "$(NuGetExePath)" 18 | mono --runtime=v4.0.30319 $(NuGetExePath) 19 | https://www.nuget.org/nuget.exe 20 | 21 | 22 | 23 | 1.1.0 24 | $(ProjectDirectory)packages\Ciao.$(CiaoVersion)\tools\Ciao.targets 25 | 26 | 27 | 28 | 29 | DownloadNuGetCommandLineClient; 30 | RestoreSolutionPackages; 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | <_CiaoRestoreSolutionPackagesCompleted>True 45 | 46 | 47 | 48 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 86 | 88 | 89 | 90 | 91 | 92 | <_CiaoProperties> 93 | ImportCiaoProperties=True; 94 | ProjectDirectory=$(ProjectDirectory); 95 | SolutionFile=$([System.IO.Path]::Combine('$(ProjectDirectory)', '$(SolutionFile)')); 96 | BuildDirectory=$(BuildDirectory); 97 | ToolsDirectory=$(ToolsDirectory); 98 | NuGetExePath=$(NuGetExePath); 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /app/services/rest-client.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import ApplicationException from 'klondike/application-exception'; 3 | import describePromise from 'klondike/util/describe-promise'; 4 | 5 | var RestApi = Ember.Service.extend({ 6 | apiKeyRequestHeaderName: 'X-NuGet-ApiKey', 7 | 8 | apiURLBinding: 'application.apiURL', 9 | apiKeyBinding: 'application.apiKey', 10 | 11 | apiInfo: {}, 12 | packageSourceUri: null, 13 | 14 | simulateRequestLatency: 0, 15 | 16 | _promise: null, 17 | 18 | init: function() { 19 | var url = this.get('apiURL') + '?nocache=' + new Date().getTime(); 20 | 21 | var deferred = Ember.RSVP.defer(describePromise(this, 'init', [url])); 22 | this.set('_promise', deferred.promise); 23 | 24 | var self = this; 25 | 26 | var data = { 27 | type: 'GET', 28 | success: function (data) { 29 | self.set('apiInfo', self._buildApiDictionary(data)); 30 | self._setPackageSource(); 31 | deferred.resolve(self); 32 | } 33 | }; 34 | 35 | Ember.$.ajax(url, data).fail(function(xhr, status) { 36 | deferred.reject('ajax call to ' + url + ' failed: ' + status + '(' + xhr.status + ')'); 37 | }); 38 | }, 39 | 40 | getApi: function (apiName, method) { 41 | var self = this; 42 | 43 | return self.get('_promise').then(function() { 44 | return self._lookupApi(apiName, method); 45 | }, null, describePromise(this, 'getApi', arguments)); 46 | }, 47 | 48 | ajax: function (apiName, options) { 49 | options = options || {}; 50 | var method; 51 | if ('type' in options) { 52 | method = options.type; 53 | } 54 | 55 | var self = this; 56 | 57 | var timeout = self.get('simulateRequestLatency') || 0; 58 | 59 | var invoke = function() { 60 | return self.getApi(apiName, method).then(function(api) { 61 | return self._invokeAjaxApi(apiName, api, options); 62 | }, null, describePromise(self, 'ajax', [apiName])); 63 | }; 64 | 65 | if (timeout) { 66 | var deferred = Ember.RSVP.defer(describePromise(this, 'ajax') + ': Simulate Request Latency'); 67 | 68 | setTimeout(function() { deferred.resolve(); }, timeout); 69 | 70 | return deferred.promise.then(invoke, null, describePromise(this, 'ajax', [apiName]) + ': Invoke'); 71 | } else { 72 | return invoke(); 73 | } 74 | }, 75 | 76 | _invokeAjaxApi: function(apiName, api, options) { 77 | if (!api) { 78 | throw new ApplicationException('Rest API method not found: ' + apiName); 79 | } 80 | 81 | var self = this; 82 | 83 | options.type = api.method; 84 | 85 | var apiKey = this.get('apiKey'); 86 | 87 | if (!Ember.isEmpty(apiKey)) { 88 | var origBeforeSend = options.beforeSend; 89 | options.beforeSend = function(xhr) { 90 | xhr.setRequestHeader(self.get('apiKeyRequestHeaderName'), apiKey); 91 | if (origBeforeSend) { 92 | origBeforeSend(xhr); 93 | } 94 | }; 95 | } 96 | 97 | var href = this._replaceParameters(api, options); 98 | 99 | return new Ember.RSVP.Promise(function(resolve, reject) { 100 | options.success = function(data) { 101 | resolve(data); 102 | }; 103 | 104 | Ember.$.ajax(href, options).fail(function(request, textStatus, errorThrown) { 105 | var error = { 106 | request: request, 107 | textStatus: textStatus, 108 | errorThrown: errorThrown }; 109 | 110 | if (request && request.status) { 111 | error.status = request.status; 112 | } 113 | 114 | if (request && request.responseJSON) { 115 | error.response = request.responseJSON; 116 | } 117 | 118 | reject(error); 119 | }); 120 | }, describePromise(this, '_invokeAjaxApi', [apiName])); 121 | }, 122 | 123 | _lookupApi: function(apiName, method) { 124 | var apiInfo = this.get('apiInfo'); 125 | apiName = apiName.toLowerCase(); 126 | 127 | if (method) { 128 | var fullKey = method + '.' + apiName; 129 | return apiInfo[fullKey]; 130 | } 131 | 132 | var pattern = new RegExp('^\\w+\\.' + apiName.replace('.', '\\.') + '$'); 133 | var matches = []; 134 | for (var key in apiInfo) { 135 | if (key.match(pattern)) { 136 | matches.push(apiInfo[key]); 137 | } 138 | } 139 | 140 | if (matches.length === 0) { 141 | throw new ApplicationException('no method matching ' + pattern); 142 | } else if (matches.length > 1) { 143 | throw new ApplicationException('multiple APIs matched ' + apiName + '; must specify HTTP method'); 144 | } 145 | 146 | return matches[0]; 147 | }, 148 | 149 | _replaceParameters: function (api, options) { 150 | // replace {foo} with options.data.foo 151 | return api.href.replace(/\{[^\}]+\}/g, function (param) { 152 | // {foo} -> foo 153 | param = param.substring(1, param.length - 1); 154 | 155 | if (!(param in options.data)) { 156 | throw new ApplicationException('Must specify required parameter "' + param + '" for REST method "' + api.name + '"'); 157 | } 158 | 159 | var value = options.data[param]; 160 | delete options.data[param]; 161 | return value; 162 | }); 163 | }, 164 | 165 | _setPackageSource: function () { 166 | var href = this._lookupApi('packages.odata', 'GET').href; 167 | 168 | this.set('packageSourceUri', href); 169 | }, 170 | 171 | _hrefToAbsolute: function(href) { 172 | if (href.indexOf('://') !== -1) { 173 | return href; 174 | } 175 | 176 | var dataUrl = this.get('_fullBaseDataUrl'); 177 | 178 | if (href[0] !== '/') { 179 | return dataUrl + href; 180 | } 181 | 182 | return dataUrl.replace(/(.+:\/\/[^/]+).*/, '$1' + href); 183 | }, 184 | 185 | _fullBaseDataUrl: function() { 186 | var base = this.get('apiURL'); 187 | 188 | if (base.indexOf('://') === -1) { 189 | base = window.location.protocol + '//' + window.location.host + base; 190 | } 191 | 192 | if (base[base.length - 1] !== '/') { 193 | base += '/'; 194 | } 195 | 196 | return base; 197 | }.property('apiURL'), 198 | 199 | _buildApiDictionary: function(data) { 200 | var apiInfo = {}; 201 | for (var i = 0; i < data.resources.length; i++) { 202 | var res = data.resources[i]; 203 | var name = res.name.toLowerCase(); 204 | for (var j = 0; j < res.actions.length; j++) { 205 | var action = res.actions[j]; 206 | var key = action.method + '.' + name.toLowerCase() + '.' + action.name.toLowerCase(); 207 | if (key in apiInfo) { 208 | console.warn('Duplicate api method: ' + key); 209 | } 210 | 211 | action.href = this._hrefToAbsolute(action.href); 212 | apiInfo[key] = action; 213 | } 214 | } 215 | return apiInfo; 216 | } 217 | }); 218 | 219 | export default RestApi; 220 | -------------------------------------------------------------------------------- /app/components/auto-complete-text-input.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | var KEYS = { 4 | TAB: 9, 5 | ENTER: 13, 6 | ESC: 27, 7 | SPACE: 32, 8 | UP: 38, 9 | DOWN: 40 10 | }; 11 | 12 | /* TODO: 13 | o blur dismisses suggestions (buggy) 14 | o handle * in numeric range query 15 | o handle conversion of "false" -> bool -> numeric value 16 | o safari & IE testing 17 | * safari: box only shows first time 18 | */ 19 | 20 | export default Ember.Component.extend({ 21 | tagName: 'div', 22 | classNames: ['auto-complete'], 23 | value: '', 24 | terms: [], 25 | suggestions: [], 26 | 27 | _selectedSuggestionIndex: -1, 28 | 29 | dropdownVisible: Ember.computed('suggestions', function() { 30 | return !Ember.isEmpty(this.get('suggestions')); 31 | }), 32 | 33 | init: function() { 34 | this._super(); 35 | 36 | // encourage property observers to start: 37 | this.get('currentTerm'); 38 | }, 39 | 40 | _cursorIndex: function() { 41 | var input = this.get('_text'); 42 | if (input) { 43 | return input.caret().start; 44 | } 45 | return 0; 46 | }.property('value'), 47 | 48 | currentTerm: function() { 49 | var value = this.get('value'); 50 | var range = this._getTermRange(); 51 | 52 | if (range.start < 0) { 53 | return ''; 54 | } 55 | 56 | return value.substring(range.start, range.end); 57 | }.property('_cursorIndex', 'value'), 58 | 59 | _getTermRange: function() { 60 | var value = this.get('value'); 61 | var end = this.get('_cursorIndex'); 62 | var start = end - 1; 63 | while (start > 0 && this._isValidTermChar(value[start])) { 64 | if (value[start] === ':') { 65 | return { start: -1, end: -1 }; 66 | } 67 | start--; 68 | } 69 | 70 | if (!this._isValidTermChar(value[start])) { 71 | start++; 72 | } 73 | 74 | return { start: start, end: end }; 75 | }, 76 | 77 | _isValidTermChar: function(c) { 78 | return c !== ' ' && c !== '+' && c !== '-' && c !== '('; 79 | }, 80 | 81 | _findSuggestions: Ember.observer('currentTerm', function() { 82 | var term = this.get('currentTerm').toLowerCase(); 83 | 84 | var matches = term === '' ? [] : this.get('terms').filter(function(i) { 85 | return i.toLowerCase().indexOf(term) >= 0; 86 | }).sort(function(a, b) { 87 | var aStartsWith = a.toLowerCase().indexOf(term) === 0; 88 | var bStartsWith = b.toLowerCase().indexOf(term) === 0; 89 | if (aStartsWith && !bStartsWith) { 90 | return -1; 91 | } else if (bStartsWith && !aStartsWith) { 92 | return 1; 93 | } 94 | 95 | return a.length - b.length; 96 | }); 97 | 98 | this.set('suggestions', matches); 99 | }), 100 | 101 | _showAutocompleteSuggestions: Ember.observer('suggestions', function() { 102 | var self = this; 103 | 104 | var list = this.$("ol"); 105 | 106 | list.empty(); 107 | 108 | this.set('_selectedSuggestionIndex', -1); 109 | 110 | this.get('suggestions').forEach(function(i) { 111 | list.append('
  • ' + i + '
  • '); 112 | }); 113 | 114 | list.children('li').on('click', function() { 115 | var value = Ember.$(this).text(); 116 | self.send('selectTerm', value); 117 | }); 118 | }), 119 | 120 | _attachEvents: function() { 121 | var self = this; 122 | var text = this.$('input[type=text]'); 123 | var list = this.$('ol'); 124 | 125 | text.on('keydown.auto-complete', function(e) { 126 | if (e.which === KEYS.DOWN) { 127 | self.send('nextSuggestion'); 128 | } else if (e.which === KEYS.UP) { 129 | self.send('previousSuggestion'); 130 | } else if (e.which === KEYS.ENTER || e.which === KEYS.TAB) { 131 | var selection = self.$('li.is-selected').first().text(); 132 | if (selection !== '') { 133 | self.send('selectTerm', selection); 134 | } else if (e.which === KEYS.ENTER) { 135 | self.sendAction(); 136 | } else { 137 | return true; 138 | } 139 | } else if (e.which === KEYS.SPACE && e.ctrlKey) { 140 | self.send('suggestAll'); 141 | } else if (e.which === KEYS.ESC) { 142 | self.send('hideSuggestions'); 143 | } else { 144 | return true; 145 | } 146 | 147 | e.preventDefault(); 148 | return false; 149 | }); 150 | 151 | text.on('blur.auto-complete', function() { 152 | if (self.get('_blurIntoOptions') === true) { 153 | self.set('_blurIntoOptions', false); 154 | return; 155 | } 156 | 157 | Ember.run.schedule('afterRender', function() { 158 | self.send('hideSuggestions'); 159 | }); 160 | }); 161 | 162 | list.on('mousedown.auto-complete', function() { 163 | self.set('_blurIntoOptions', true); 164 | }); 165 | 166 | this.set('_text', text); 167 | }.on('didInsertElement'), 168 | 169 | _unattachEvents: function() { 170 | var text = this.$('input[type=text]'); 171 | text.off('keydown.auto-complete'); 172 | text.off('blur.auto-complete'); 173 | }.on('willDestroyElement'), 174 | 175 | _advanceSuggestion: function(delta, absolute) { 176 | var i = this.get('_selectedSuggestionIndex'); 177 | var next = absolute !== undefined ? absolute : i + delta; 178 | 179 | var opts = this.$('li'); 180 | if (opts.length === 0) { 181 | return; 182 | } 183 | 184 | if (next < 0) { 185 | next = 0; 186 | } else if (next >= opts.length) { 187 | next = opts.length - 1; 188 | } 189 | 190 | if (i >= 0 && i < opts.length) { 191 | Ember.$(opts[i]).removeClass('is-selected'); 192 | } 193 | Ember.$(opts[next]).addClass('is-selected'); 194 | 195 | this.set('_selectedSuggestionIndex', next); 196 | }, 197 | 198 | _scrollToSelectedIndex: Ember.observer('_selectedSuggestionIndex', function() { 199 | var list = this.$('ol.auto-complete'); 200 | var selected = this.$('li.is-selected'); 201 | if (selected.length !== 1) { 202 | list.scrollTop(0); 203 | return; 204 | } 205 | 206 | var offset = selected.offset().top - list.offset().top; 207 | 208 | if (offset < 0) { 209 | list.scrollTop(list.scrollTop() + offset); 210 | } else if (offset + selected.outerHeight() > list.height()) { 211 | var pos = offset + selected.outerHeight() - list.height(); 212 | list.scrollTop(list.scrollTop() + pos); 213 | } 214 | }), 215 | 216 | actions: { 217 | selectTerm: function(term) { 218 | var value = this.get('value'); 219 | var range = this._getTermRange(); 220 | var up = value.substring(0, range.start) + term + ':'; 221 | 222 | var caretPosition = up.length; 223 | 224 | if (range.end < value.length) { 225 | var end = value.substring(range.end, value.length); 226 | if (end.length > 0 && end[0] !== ' ') { 227 | end = ' ' + end; 228 | } 229 | up += end; 230 | } 231 | 232 | this.set('value', up); 233 | 234 | this.set('suggestions', []); 235 | 236 | var text = this.get('_text'); 237 | 238 | Ember.run.schedule('afterRender', function() { 239 | text.caret(caretPosition, caretPosition); 240 | }); 241 | }, 242 | 243 | nextSuggestion: function() { 244 | this._advanceSuggestion(1); 245 | }, 246 | 247 | previousSuggestion: function() { 248 | this._advanceSuggestion(-1); 249 | }, 250 | 251 | suggestAll: function() { 252 | this.set('suggestions', this.get('terms').sort()); 253 | this._advanceSuggestion(0, 0); 254 | }, 255 | 256 | hideSuggestions: function() { 257 | this.set('suggestions', []); 258 | } 259 | } 260 | }); 261 | --------------------------------------------------------------------------------