├── 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 |
2 | {{input type="checkbox" name=name checked=checked}}
3 | {{label}}
4 |
--------------------------------------------------------------------------------
/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 |
3 | {{item.label}}
4 |
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 | {{attrs.header}}:
4 | {{/if}}
5 |
6 | {{#each attrs.items as |item|}}
7 |
8 | {{#link-to 'packages.search' (query-params query=item page=0) rel="nofollow"}}{{item}}{{/link-to}}
9 |
10 | {{/each}}
11 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
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 |
5 | {{#each model as |user|}}
6 |
7 | {{#if canEdit}}
8 | {{#link-to 'users.edit' user}}{{user.username}}{{/link-to}}
9 | {{else}}
10 | {{user.username}}
11 | {{/if}}
12 |
13 | {{/each}}
14 |
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 |
14 | Username
15 | {{focus-input type="text" value=username enter="logIn"}}
16 |
17 |
18 | Password
19 | {{input type="password" value=password enter="logIn"}}
20 |
21 |
22 |
Log in
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 | Search
17 |
18 |
19 |
20 |
Sample Queries
21 |
22 |
23 |
24 | Query
25 | Description
26 |
27 |
28 |
29 | {{#each examples as |example|}}
30 |
31 | {{example.query}}
32 | {{example.description}}
33 | Try it
34 |
35 | {{/each}}
36 |
37 |
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 |
Reveal
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 |
Change API Key
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 | Synchronize Packages
16 | Rebuild Index
17 | Cancel
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 | Username
8 | {{focus-input type="text" value=model.username class="field col-12 block"}}
9 |
10 |
11 | Api Key
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 | Save
29 |
30 | {{#if canDelete}}
31 | Delete
32 | {{/if}}
33 |
34 | Cancel
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 |
41 |
42 |
43 | {{#each model.hits as |hit|}}
44 |
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 |
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 | {{set.targetFramework.fullName}}
60 |
61 | {{#each set.dependencies as |dep|}}
62 |
63 |
64 | {{dep.id}}
65 |
66 | {{dep.versionSpec.minVersion}}
67 |
68 | {{/each}}
69 | {{/each}}
70 |
71 |
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 | Version
91 | Downloads
92 | Last Update
93 |
94 |
95 | {{#each model.versionHistory as |item|}}
96 |
97 | {{#link-to 'packages.view' item}}{{item.version}}{{/link-to}}
98 | {{item.versionDownloadCount}}
99 | {{format-date item.lastUpdated}}
100 |
101 | {{/each}}
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | {{#if model.copyright}}
110 |
{{model.copyright}}
111 | {{/if}}
112 |
113 | {{#if model.isMirrored}}
114 |
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 [](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 |
--------------------------------------------------------------------------------