feat(api): decouple api from curriculum (#40703)
This commit is contained in:
committed by
GitHub
parent
f4bbe3f34c
commit
c077ffe4b9
7
api-server/src/common/config.global.js
Normal file
7
api-server/src/common/config.global.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// The path where to mount the REST API app
|
||||
exports.restApiRoot = '/api';
|
||||
//
|
||||
// The URL where the browser client can access the REST API is available
|
||||
// Replace with a full url (including hostname) if your client is being
|
||||
// served from a different server than your REST API.
|
||||
exports.restApiUrl = exports.restApiRoot;
|
||||
1
api-server/src/common/index.less
Normal file
1
api-server/src/common/index.less
Normal file
@@ -0,0 +1 @@
|
||||
&{ @import "./app/index.less"; }
|
||||
94
api-server/src/common/models/User-Credential.js
Normal file
94
api-server/src/common/models/User-Credential.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Observable } from 'rx';
|
||||
import debug from 'debug';
|
||||
|
||||
import { observeMethod, observeQuery } from '../../server/utils/rx';
|
||||
import {
|
||||
createUserUpdatesFromProfile,
|
||||
getSocialProvider
|
||||
} from '../../server/utils/auth';
|
||||
|
||||
const log = debug('fcc:models:UserCredential');
|
||||
module.exports = function(UserCredential) {
|
||||
UserCredential.link = function(
|
||||
userId,
|
||||
_provider,
|
||||
authScheme,
|
||||
profile,
|
||||
credentials,
|
||||
options = {},
|
||||
cb
|
||||
) {
|
||||
if (typeof options === 'function' && !cb) {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
const User = UserCredential.app.models.User;
|
||||
const findCred = observeMethod(UserCredential, 'findOne');
|
||||
const createCred = observeMethod(UserCredential, 'create');
|
||||
|
||||
const provider = getSocialProvider(_provider);
|
||||
const query = {
|
||||
where: {
|
||||
provider: provider,
|
||||
externalId: profile.id
|
||||
}
|
||||
};
|
||||
|
||||
// find createCred if they exist
|
||||
// if not create it
|
||||
// if yes, update credentials
|
||||
// also if github
|
||||
// update profile
|
||||
// update username
|
||||
// update picture
|
||||
log('link query', query);
|
||||
return findCred(query)
|
||||
.flatMap(_credentials => {
|
||||
const modified = new Date();
|
||||
const updateUser = new Promise((resolve, reject) => {
|
||||
User.find({ id: userId }, (err, user) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return user.updateAttributes(
|
||||
createUserUpdatesFromProfile(provider, profile),
|
||||
updateErr => {
|
||||
if (updateErr) {
|
||||
return reject(updateErr);
|
||||
}
|
||||
return resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
let updateCredentials;
|
||||
if (!_credentials) {
|
||||
updateCredentials = createCred({
|
||||
provider,
|
||||
externalId: profile.id,
|
||||
authScheme,
|
||||
// we no longer want to keep the profile
|
||||
// this is information we do not need or use
|
||||
profile: null,
|
||||
credentials,
|
||||
userId,
|
||||
created: modified,
|
||||
modified
|
||||
});
|
||||
} else {
|
||||
_credentials.credentials = credentials;
|
||||
updateCredentials = observeQuery(_credentials, 'updateAttributes', {
|
||||
profile: null,
|
||||
credentials,
|
||||
modified
|
||||
});
|
||||
}
|
||||
return Observable.combineLatest(
|
||||
Observable.fromPromise(updateUser),
|
||||
updateCredentials,
|
||||
(_, credentials) => credentials
|
||||
);
|
||||
})
|
||||
.subscribe(credentials => cb(null, credentials), cb);
|
||||
};
|
||||
};
|
||||
16
api-server/src/common/models/User-Credential.json
Normal file
16
api-server/src/common/models/User-Credential.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "userCredential",
|
||||
"plural": "userCredentials",
|
||||
"base": "UserCredential",
|
||||
"properties": {},
|
||||
"validations": [],
|
||||
"relations": {
|
||||
"user": {
|
||||
"type": "belongsTo",
|
||||
"model": "user",
|
||||
"foreignKey": "userId"
|
||||
}
|
||||
},
|
||||
"acls": [],
|
||||
"methods": {}
|
||||
}
|
||||
158
api-server/src/common/models/User-Identity.js
Normal file
158
api-server/src/common/models/User-Identity.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import { Observable } from 'rx';
|
||||
// import debug from 'debug';
|
||||
import dedent from 'dedent';
|
||||
import { isEmail } from 'validator';
|
||||
|
||||
import { observeMethod, observeQuery } from '../../server/utils/rx';
|
||||
import { wrapHandledError } from '../../server/utils/create-handled-error.js';
|
||||
|
||||
// const log = debug('fcc:models:userIdent');
|
||||
|
||||
export function ensureLowerCaseEmail(profile) {
|
||||
return typeof profile?.emails?.[0]?.value === 'string'
|
||||
? profile.emails[0].value.toLowerCase()
|
||||
: '';
|
||||
}
|
||||
|
||||
export default function(UserIdent) {
|
||||
UserIdent.on('dataSourceAttached', () => {
|
||||
UserIdent.findOne$ = observeMethod(UserIdent, 'findOne');
|
||||
});
|
||||
|
||||
UserIdent.login = function(
|
||||
_provider,
|
||||
authScheme,
|
||||
profile,
|
||||
credentials,
|
||||
options,
|
||||
cb
|
||||
) {
|
||||
const User = UserIdent.app.models.User;
|
||||
const AccessToken = UserIdent.app.models.AccessToken;
|
||||
options = options || {};
|
||||
if (typeof options === 'function' && !cb) {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
// get the social provider data and the external id from auth0
|
||||
profile.id = profile.id || profile.openid;
|
||||
const auth0IdString = '' + profile.id;
|
||||
const [provider, socialExtId] = auth0IdString.split('|');
|
||||
const query = {
|
||||
where: {
|
||||
provider: provider,
|
||||
externalId: socialExtId
|
||||
},
|
||||
include: 'user'
|
||||
};
|
||||
// get the email from the auth0 (its expected from social providers)
|
||||
const email = ensureLowerCaseEmail(profile);
|
||||
|
||||
if (!isEmail('' + email)) {
|
||||
throw wrapHandledError(
|
||||
new Error('invalid or empty email received from auth0'),
|
||||
{
|
||||
message: dedent`
|
||||
${provider} did not return a valid email address.
|
||||
Please try again with a different account that has an
|
||||
email associated with it your update your settings on ${provider}, for us to be able to retrieve your email.
|
||||
`,
|
||||
type: 'info',
|
||||
redirectTo: '/'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (provider === 'email') {
|
||||
return User.findOne$({ where: { email } })
|
||||
.flatMap(user => {
|
||||
return user
|
||||
? Observable.of(user)
|
||||
: User.create$({ email }).toPromise();
|
||||
})
|
||||
.flatMap(user => {
|
||||
if (!user) {
|
||||
throw wrapHandledError(
|
||||
new Error('could not find or create a user'),
|
||||
{
|
||||
message: dedent`
|
||||
We could not find or create a user with that email address.
|
||||
`,
|
||||
type: 'info',
|
||||
redirectTo: '/'
|
||||
}
|
||||
);
|
||||
}
|
||||
const createToken = observeQuery(AccessToken, 'create', {
|
||||
userId: user.id,
|
||||
created: new Date(),
|
||||
ttl: user.constructor.settings.ttl
|
||||
});
|
||||
const updateUserPromise = new Promise((resolve, reject) =>
|
||||
user.updateAttributes(
|
||||
{
|
||||
emailVerified: true,
|
||||
emailAuthLinkTTL: null,
|
||||
emailVerifyTTL: null
|
||||
},
|
||||
err => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve();
|
||||
}
|
||||
)
|
||||
);
|
||||
return Observable.combineLatest(
|
||||
Observable.of(user),
|
||||
createToken,
|
||||
Observable.fromPromise(updateUserPromise),
|
||||
(user, token) => ({ user, token })
|
||||
);
|
||||
})
|
||||
.subscribe(({ user, token }) => cb(null, user, null, token), cb);
|
||||
} else {
|
||||
return UserIdent.findOne$(query)
|
||||
.flatMap(identity => {
|
||||
return identity
|
||||
? Observable.of(identity.user())
|
||||
: User.findOne$({ where: { email } }).flatMap(user => {
|
||||
return user
|
||||
? Observable.of(user)
|
||||
: User.create$({ email }).toPromise();
|
||||
});
|
||||
})
|
||||
.flatMap(user => {
|
||||
const createToken = observeQuery(AccessToken, 'create', {
|
||||
userId: user.id,
|
||||
created: new Date(),
|
||||
ttl: user.constructor.settings.ttl
|
||||
});
|
||||
const updateUser = new Promise((resolve, reject) =>
|
||||
user.updateAttributes(
|
||||
{
|
||||
email: email,
|
||||
emailVerified: true,
|
||||
emailAuthLinkTTL: null,
|
||||
emailVerifyTTL: null
|
||||
},
|
||||
err => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve();
|
||||
}
|
||||
)
|
||||
);
|
||||
return Observable.combineLatest(
|
||||
Observable.of(user),
|
||||
createToken,
|
||||
Observable.fromPromise(updateUser),
|
||||
(user, token) => ({ user, token })
|
||||
);
|
||||
})
|
||||
.subscribe(({ user, token }) => cb(null, user, null, token), cb);
|
||||
}
|
||||
};
|
||||
}
|
||||
23
api-server/src/common/models/User-Identity.json
Normal file
23
api-server/src/common/models/User-Identity.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "userIdentity",
|
||||
"plural": "userIdentities",
|
||||
"base": "UserIdentity",
|
||||
"properties": {},
|
||||
"validations": [],
|
||||
"relations": {
|
||||
"user": {
|
||||
"type": "belongsTo",
|
||||
"model": "user",
|
||||
"foreignKey": "userId"
|
||||
}
|
||||
},
|
||||
"acls": [
|
||||
{
|
||||
"accessType": "*",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY"
|
||||
}
|
||||
],
|
||||
"methods": {}
|
||||
}
|
||||
30
api-server/src/common/models/User-Identity.test.js
Normal file
30
api-server/src/common/models/User-Identity.test.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/* global expect */
|
||||
import { ensureLowerCaseEmail } from './User-Identity';
|
||||
|
||||
test('returns lowercase email when one exists', () => {
|
||||
const profile = {
|
||||
id: 2,
|
||||
emails: [{ value: 'Example@Mail.com', name: 'John Doe' }]
|
||||
};
|
||||
expect(ensureLowerCaseEmail(profile)).toBe('example@mail.com');
|
||||
});
|
||||
|
||||
test('returns empty string when value is undefined', () => {
|
||||
const profile = {
|
||||
id: 4,
|
||||
emails: []
|
||||
};
|
||||
expect(ensureLowerCaseEmail(profile)).toBe('');
|
||||
});
|
||||
|
||||
test('returns empty string when emails is undefined', () => {
|
||||
const profile = {
|
||||
id: 5
|
||||
};
|
||||
expect(ensureLowerCaseEmail(profile)).toBe('');
|
||||
});
|
||||
|
||||
test('returns empty string when profile is undefined', () => {
|
||||
let profile;
|
||||
expect(ensureLowerCaseEmail(profile)).toBe('');
|
||||
});
|
||||
9
api-server/src/common/models/article.js
Normal file
9
api-server/src/common/models/article.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Observable } from 'rx';
|
||||
|
||||
module.exports = function(Article) {
|
||||
Article.on('dataSourceAttached', () => {
|
||||
Article.findOne$ = Observable.fromNodeCallback(Article.findOne, Article);
|
||||
Article.findById$ = Observable.fromNodeCallback(Article.findById, Article);
|
||||
Article.find$ = Observable.fromNodeCallback(Article.find, Article);
|
||||
});
|
||||
};
|
||||
102
api-server/src/common/models/article.json
Normal file
102
api-server/src/common/models/article.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"name": "article",
|
||||
"plural": "articles",
|
||||
"base": "PersistedModel",
|
||||
"idInjection": true,
|
||||
"options": {
|
||||
"validateUpsert": true
|
||||
},
|
||||
"properties": {
|
||||
"shortId": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"slugPart": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "A kebab-case-string created from the title, will have the shortId appended to it"
|
||||
},
|
||||
"meta": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"description": "A place to keep the referral link and read time"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"author": {
|
||||
"type": "object",
|
||||
"required": true
|
||||
},
|
||||
"subtitle": {
|
||||
"type": "string"
|
||||
},
|
||||
"featureImage": {
|
||||
"type": "object"
|
||||
},
|
||||
"draft": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"renderableContent": {
|
||||
"type": "string"
|
||||
},
|
||||
"youtubeId": {
|
||||
"type": "string",
|
||||
"description": "A youtube video id eg: 'EErY9zXGLNU'"
|
||||
},
|
||||
"published": {
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"default": false
|
||||
},
|
||||
"featured": {
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"default": false
|
||||
},
|
||||
"underReview": {
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"default": false
|
||||
},
|
||||
"viewCount": {
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"default": 1
|
||||
},
|
||||
"firstPublishedDate": {
|
||||
"type": "date"
|
||||
},
|
||||
"createdDate": {
|
||||
"type": "date",
|
||||
"required": true
|
||||
},
|
||||
"lastEditedDate": {
|
||||
"type": "date",
|
||||
"required": true
|
||||
},
|
||||
"history": {
|
||||
"type": [
|
||||
"object"
|
||||
],
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"validations": [],
|
||||
"relations": {
|
||||
"user": {
|
||||
"type": "belongsTo",
|
||||
"model": "user",
|
||||
"foreignKey": "externalId"
|
||||
},
|
||||
"popularity": {
|
||||
"type": "hasOne",
|
||||
"model": "popularity",
|
||||
"foreignKey": "popularityId"
|
||||
}
|
||||
},
|
||||
"acls": [],
|
||||
"methods": {}
|
||||
}
|
||||
9
api-server/src/common/models/block.js
Normal file
9
api-server/src/common/models/block.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Observable } from 'rx';
|
||||
|
||||
export default function(Block) {
|
||||
Block.on('dataSourceAttached', () => {
|
||||
Block.findOne$ = Observable.fromNodeCallback(Block.findOne, Block);
|
||||
Block.findById$ = Observable.fromNodeCallback(Block.findById, Block);
|
||||
Block.find$ = Observable.fromNodeCallback(Block.find, Block);
|
||||
});
|
||||
}
|
||||
53
api-server/src/common/models/block.json
Normal file
53
api-server/src/common/models/block.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "block",
|
||||
"base": "PersistedModel",
|
||||
"idInjection": true,
|
||||
"options": {
|
||||
"validateUpsert": true
|
||||
},
|
||||
"properties": {
|
||||
"superBlock": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "The super block that this block belongs to"
|
||||
},
|
||||
"order": {
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"description": "The order in which this block appears"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "The name of this block derived from the title, suitable for regex search"
|
||||
},
|
||||
"superOrder": {
|
||||
"type": "number",
|
||||
"required": true
|
||||
},
|
||||
"dashedName": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Generated from the title to be URL friendly"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "The title of this block, suitable for display"
|
||||
},
|
||||
"time": {
|
||||
"type": "string",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"validations": [],
|
||||
"relations": {
|
||||
"challenges": {
|
||||
"type": "hasMany",
|
||||
"model": "challenge",
|
||||
"foreignKey": "blockId"
|
||||
}
|
||||
},
|
||||
"acls": [],
|
||||
"methods": {}
|
||||
}
|
||||
139
api-server/src/common/models/challenge.json
Normal file
139
api-server/src/common/models/challenge.json
Normal file
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"name": "challenge",
|
||||
"base": "PersistedModel",
|
||||
"idInjection": true,
|
||||
"trackChanges": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"id": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"index": {
|
||||
"mongodb": {
|
||||
"unique": true,
|
||||
"background": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"type": "number"
|
||||
},
|
||||
"suborder": {
|
||||
"type": "number"
|
||||
},
|
||||
"checksum": {
|
||||
"type": "number"
|
||||
},
|
||||
"isBeta": {
|
||||
"type": "boolean",
|
||||
"description": "Show only during dev or on beta site. Completely omitted otherwise"
|
||||
},
|
||||
"isComingSoon": {
|
||||
"type": "boolean",
|
||||
"description": "Challenge shows in production, but is unreachable and disabled. Is reachable in beta/dev only if isBeta flag is set"
|
||||
},
|
||||
"dashedName": {
|
||||
"type": "string"
|
||||
},
|
||||
"superBlock": {
|
||||
"type": "string",
|
||||
"description": "Used for ordering challenge blocks in map"
|
||||
},
|
||||
"superOrder": {
|
||||
"type": "number",
|
||||
"description": "Used to determine super block order, set by prepending two digit number to super block folder name"
|
||||
},
|
||||
"block": {
|
||||
"type": "string"
|
||||
},
|
||||
"difficulty": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"tests": {
|
||||
"type": "array"
|
||||
},
|
||||
"head": {
|
||||
"type": "string",
|
||||
"description": "Appended to user code",
|
||||
"default": ""
|
||||
},
|
||||
"tail": {
|
||||
"type": "string",
|
||||
"description": "Prepended to user code",
|
||||
"default": ""
|
||||
},
|
||||
"helpRoom": {
|
||||
"type": "string",
|
||||
"description": "Gitter help chatroom this challenge belongs too. Must be PascalCase",
|
||||
"default": "Help"
|
||||
},
|
||||
"fileName": {
|
||||
"type": "string",
|
||||
"description": "Filename challenge comes from. Used in dev mode"
|
||||
},
|
||||
"challengeSeed": {
|
||||
"type": "array"
|
||||
},
|
||||
"challengeType": {
|
||||
"type": "number"
|
||||
},
|
||||
"solutions": {
|
||||
"type": "array",
|
||||
"default": []
|
||||
},
|
||||
"guideUrl": {
|
||||
"type": "string",
|
||||
"description": "Used to link to an article in the FCC guide"
|
||||
},
|
||||
"required": {
|
||||
"type": [
|
||||
{
|
||||
"type": {
|
||||
"link": {
|
||||
"type": "string",
|
||||
"description": "Used for css files"
|
||||
},
|
||||
"src": {
|
||||
"type": "string",
|
||||
"description": "Used for script files"
|
||||
},
|
||||
"crossDomain": {
|
||||
"type": "boolean",
|
||||
"description": "Files coming from freeCodeCamp must mark this true"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"default": []
|
||||
},
|
||||
"template": {
|
||||
"type": "string",
|
||||
"description": "A template to render the compiled challenge source into. This template uses template literal delimiter, i.e. ${ foo }"
|
||||
}
|
||||
},
|
||||
"validations": [],
|
||||
"relations": {},
|
||||
"acls": [
|
||||
{
|
||||
"accessType": "*",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY"
|
||||
},
|
||||
{
|
||||
"accessType": "READ",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
}
|
||||
],
|
||||
"methods": {}
|
||||
}
|
||||
68
api-server/src/common/models/nonprofit.json
Normal file
68
api-server/src/common/models/nonprofit.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "nonprofit",
|
||||
"base": "PersistedModel",
|
||||
"idInjection": true,
|
||||
"trackChanges": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"id": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"index": {
|
||||
"mongodb": {
|
||||
"unique": true,
|
||||
"background": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"whatDoesNonprofitDo": {
|
||||
"type": "string"
|
||||
},
|
||||
"websiteLink": {
|
||||
"type": "string"
|
||||
},
|
||||
"endUser": {
|
||||
"type": "string"
|
||||
},
|
||||
"approvedDeliverables": {
|
||||
"type": "array"
|
||||
},
|
||||
"projectDescription": {
|
||||
"type": "string"
|
||||
},
|
||||
"logoUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"estimatedHours": {
|
||||
"type": "number"
|
||||
},
|
||||
"moneySaved": {
|
||||
"type": "number"
|
||||
},
|
||||
"currentStatus": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"validations": [],
|
||||
"relations": {},
|
||||
"acls": [
|
||||
{
|
||||
"accessType": "*",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY"
|
||||
},
|
||||
{
|
||||
"accessType": "READ",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
}
|
||||
],
|
||||
"methods": {}
|
||||
}
|
||||
55
api-server/src/common/models/pledge.json
Normal file
55
api-server/src/common/models/pledge.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "pledge",
|
||||
"base": "PersistedModel",
|
||||
"idInjection": true,
|
||||
"trackChanges": false,
|
||||
"properties": {
|
||||
"nonprofit": {
|
||||
"type": "string",
|
||||
"index": true
|
||||
},
|
||||
"amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"dateStarted": {
|
||||
"type": "date",
|
||||
"defaultFn": "now"
|
||||
},
|
||||
"dateEnded": {
|
||||
"type": "date"
|
||||
},
|
||||
"formerUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"isOrphaned": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isCompleted": {
|
||||
"type": "boolean",
|
||||
"default": "false"
|
||||
}
|
||||
},
|
||||
"validations": [],
|
||||
"relations": {
|
||||
"user": {
|
||||
"type": "hasMany",
|
||||
"model": "user",
|
||||
"foreignKey": "userId"
|
||||
}
|
||||
},
|
||||
"acls": [
|
||||
{
|
||||
"accessType": "*",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY"
|
||||
},
|
||||
{
|
||||
"accessType": "READ",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
}
|
||||
],
|
||||
"methods": {}
|
||||
}
|
||||
15
api-server/src/common/models/popularity.js
Normal file
15
api-server/src/common/models/popularity.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Observable } from 'rx';
|
||||
|
||||
module.exports = function(Popularity) {
|
||||
Popularity.on('dataSourceAttached', () => {
|
||||
Popularity.findOne$ = Observable.fromNodeCallback(
|
||||
Popularity.findOne,
|
||||
Popularity
|
||||
);
|
||||
Popularity.findById$ = Observable.fromNodeCallback(
|
||||
Popularity.findById,
|
||||
Popularity
|
||||
);
|
||||
Popularity.find$ = Observable.fromNodeCallback(Popularity.find, Popularity);
|
||||
});
|
||||
};
|
||||
28
api-server/src/common/models/popularity.json
Normal file
28
api-server/src/common/models/popularity.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "popularity",
|
||||
"plural": "popularities",
|
||||
"base": "PersistedModel",
|
||||
"idInjection": true,
|
||||
"options": {
|
||||
"validateUpsert": true
|
||||
},
|
||||
"properties": {
|
||||
"events": {
|
||||
"type": [
|
||||
"object"
|
||||
],
|
||||
"required": true,
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"validations": [],
|
||||
"relations": {
|
||||
"article": {
|
||||
"type": "belongsTo",
|
||||
"model": "article",
|
||||
"foreignKey": "articleId"
|
||||
}
|
||||
},
|
||||
"acls": [],
|
||||
"methods": {}
|
||||
}
|
||||
1058
api-server/src/common/models/user.js
Normal file
1058
api-server/src/common/models/user.js
Normal file
File diff suppressed because it is too large
Load Diff
402
api-server/src/common/models/user.json
Normal file
402
api-server/src/common/models/user.json
Normal file
@@ -0,0 +1,402 @@
|
||||
{
|
||||
"name": "user",
|
||||
"base": "User",
|
||||
"strict": "filter",
|
||||
"idInjection": true,
|
||||
"emailVerificationRequired": false,
|
||||
"trackChanges": false,
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"index": {
|
||||
"mongodb": {
|
||||
"unique": true,
|
||||
"background": true,
|
||||
"sparse": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"newEmail": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailVerifyTTL": {
|
||||
"type": "date"
|
||||
},
|
||||
"emailVerified": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"emailAuthLinkTTL": {
|
||||
"type": "date"
|
||||
},
|
||||
"externalId": {
|
||||
"type": "string",
|
||||
"description": "A uuid/v4 used to identify user accounts"
|
||||
},
|
||||
"unsubscribeId": {
|
||||
"type": "string",
|
||||
"description": "An ObjectId used to unsubscribe users from the mailing list(s)"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"description": "No longer used for new accounts"
|
||||
},
|
||||
"progressTimestamps": {
|
||||
"type": "array",
|
||||
"default": []
|
||||
},
|
||||
"isBanned": {
|
||||
"type": "boolean",
|
||||
"description": "User is banned from posting to camper news",
|
||||
"default": false
|
||||
},
|
||||
"isCheater": {
|
||||
"type": "boolean",
|
||||
"description": "Users who are confirmed to have broken academic honesty policy are marked as cheaters",
|
||||
"default": false
|
||||
},
|
||||
"githubProfile": {
|
||||
"type": "string"
|
||||
},
|
||||
"website": {
|
||||
"type": "string"
|
||||
},
|
||||
"_csrf": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"index": {
|
||||
"mongodb": {
|
||||
"unique": true,
|
||||
"background": true
|
||||
}
|
||||
},
|
||||
"require": true
|
||||
},
|
||||
"about": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"picture": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"linkedin": {
|
||||
"type": "string"
|
||||
},
|
||||
"codepen": {
|
||||
"type": "string"
|
||||
},
|
||||
"twitter": {
|
||||
"type": "string"
|
||||
},
|
||||
"acceptedPrivacyTerms": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"sendQuincyEmail": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"currentChallengeId": {
|
||||
"type": "string",
|
||||
"description": "The challenge last visited by the user",
|
||||
"default": ""
|
||||
},
|
||||
"isHonest": {
|
||||
"type": "boolean",
|
||||
"description": "Camper has signed academic honesty policy",
|
||||
"default": false
|
||||
},
|
||||
"isFrontEndCert": {
|
||||
"type": "boolean",
|
||||
"description": "Camper is front end certified",
|
||||
"default": false
|
||||
},
|
||||
"isDataVisCert": {
|
||||
"type": "boolean",
|
||||
"description": "Camper is data visualization certified",
|
||||
"default": false
|
||||
},
|
||||
"isBackEndCert": {
|
||||
"type": "boolean",
|
||||
"description": "Campers is back end certified",
|
||||
"default": false
|
||||
},
|
||||
"isFullStackCert": {
|
||||
"type": "boolean",
|
||||
"description": "Campers is full stack certified",
|
||||
"default": false
|
||||
},
|
||||
"isRespWebDesignCert": {
|
||||
"type": "boolean",
|
||||
"description": "Camper is responsive web design certified",
|
||||
"default": false
|
||||
},
|
||||
"is2018DataVisCert": {
|
||||
"type": "boolean",
|
||||
"description": "Camper is data visualization certified (2018)",
|
||||
"default": false
|
||||
},
|
||||
"isFrontEndLibsCert": {
|
||||
"type": "boolean",
|
||||
"description": "Camper is front end libraries certified",
|
||||
"default": false
|
||||
},
|
||||
"isJsAlgoDataStructCert": {
|
||||
"type": "boolean",
|
||||
"description": "Camper is javascript algorithms and data structures certified",
|
||||
"default": false
|
||||
},
|
||||
"isApisMicroservicesCert": {
|
||||
"type": "boolean",
|
||||
"description": "Camper is apis and microservices certified",
|
||||
"default": false
|
||||
},
|
||||
"isInfosecQaCert": {
|
||||
"type": "boolean",
|
||||
"description": "Camper is information security and quality assurance certified",
|
||||
"default": false
|
||||
},
|
||||
"isQaCertV7": {
|
||||
"type": "boolean",
|
||||
"description": "Camper is quality assurance certified",
|
||||
"default": false
|
||||
},
|
||||
"isInfosecCertV7": {
|
||||
"type": "boolean",
|
||||
"description": "Camper is information security certified",
|
||||
"default": false
|
||||
},
|
||||
"is2018FullStackCert": {
|
||||
"type": "boolean",
|
||||
"description": "Camper is full stack certified (2018)",
|
||||
"default": false
|
||||
},
|
||||
"isSciCompPyCertV7": {
|
||||
"type": "boolean",
|
||||
"description": "Camper is scientific computing with Python certified",
|
||||
"default": false
|
||||
},
|
||||
"isDataAnalysisPyCertV7": {
|
||||
"type": "boolean",
|
||||
"description": "Camper is data analysis with Python certified",
|
||||
"default": false
|
||||
},
|
||||
"isMachineLearningPyCertV7": {
|
||||
"type": "boolean",
|
||||
"description": "Camper is machine learning with Python certified",
|
||||
"default": false
|
||||
},
|
||||
"completedChallenges": {
|
||||
"type": [
|
||||
{
|
||||
"completedDate": "number",
|
||||
"id": "string",
|
||||
"solution": "string",
|
||||
"githubLink": "string",
|
||||
"challengeType": "number",
|
||||
"files": {
|
||||
"type": [
|
||||
{
|
||||
"contents": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"ext": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"default": []
|
||||
},
|
||||
"portfolio": {
|
||||
"type": "array",
|
||||
"default": []
|
||||
},
|
||||
"yearsTopContributor": {
|
||||
"type": "array",
|
||||
"default": []
|
||||
},
|
||||
"rand": {
|
||||
"type": "number",
|
||||
"index": true
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
},
|
||||
"theme": {
|
||||
"type": "string",
|
||||
"default": "default"
|
||||
},
|
||||
"profileUI": {
|
||||
"type": "object",
|
||||
"default": {
|
||||
"isLocked": true,
|
||||
"showAbout": false,
|
||||
"showCerts": false,
|
||||
"showDonation": false,
|
||||
"showHeatMap": false,
|
||||
"showLocation": false,
|
||||
"showName": false,
|
||||
"showPoints": false,
|
||||
"showPortfolio": false,
|
||||
"showTimeLine": false
|
||||
}
|
||||
},
|
||||
"badges": {
|
||||
"type": {
|
||||
"coreTeam": {
|
||||
"type": "array",
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"default": {}
|
||||
},
|
||||
"donationEmails": {
|
||||
"type": [
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"isDonating": {
|
||||
"type": "boolean",
|
||||
"description": "Does the camper have an active donation",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"validations": [],
|
||||
"relations": {
|
||||
"donations": {
|
||||
"type": "hasMany",
|
||||
"foreignKey": "",
|
||||
"modal": "donation"
|
||||
},
|
||||
"credentials": {
|
||||
"type": "hasMany",
|
||||
"model": "userCredential",
|
||||
"foreignKey": ""
|
||||
},
|
||||
"identities": {
|
||||
"type": "hasMany",
|
||||
"model": "userIdentity",
|
||||
"foreignKey": ""
|
||||
},
|
||||
"pledge": {
|
||||
"type": "hasOne",
|
||||
"model": "pledge",
|
||||
"foreignKey": ""
|
||||
},
|
||||
"authTokens": {
|
||||
"type": "hasMany",
|
||||
"model": "AuthToken",
|
||||
"foreignKey": "userId",
|
||||
"options": {
|
||||
"disableInclude": true
|
||||
}
|
||||
},
|
||||
"articles": {
|
||||
"type": "hasMany",
|
||||
"model": "article",
|
||||
"foreignKey": "externalId"
|
||||
}
|
||||
},
|
||||
"acls": [
|
||||
{
|
||||
"accessType": "*",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY",
|
||||
"property": "create"
|
||||
},
|
||||
{
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY",
|
||||
"property": "login"
|
||||
},
|
||||
{
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY",
|
||||
"property": "verify"
|
||||
},
|
||||
{
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "DENY",
|
||||
"property": "resetPassword"
|
||||
},
|
||||
{
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW",
|
||||
"property": "doesExist"
|
||||
},
|
||||
{
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW",
|
||||
"property": "about"
|
||||
},
|
||||
{
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW",
|
||||
"property": "getPublicProfile"
|
||||
},
|
||||
{
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW",
|
||||
"property": "giveBrowniePoints"
|
||||
},
|
||||
{
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$owner",
|
||||
"permission": "ALLOW",
|
||||
"property": "updateTheme"
|
||||
},
|
||||
{
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW",
|
||||
"property": "getMessages"
|
||||
}
|
||||
],
|
||||
"methods": {}
|
||||
}
|
||||
80
api-server/src/common/utils/auth.js
Normal file
80
api-server/src/common/utils/auth.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import path from 'path';
|
||||
|
||||
import dedent from 'dedent';
|
||||
import loopback from 'loopback';
|
||||
import moment from 'moment';
|
||||
|
||||
export const renderSignUpEmail = loopback.template(
|
||||
path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'server',
|
||||
'views',
|
||||
'emails',
|
||||
'user-request-sign-up.ejs'
|
||||
)
|
||||
);
|
||||
|
||||
export const renderSignInEmail = loopback.template(
|
||||
path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'server',
|
||||
'views',
|
||||
'emails',
|
||||
'user-request-sign-in.ejs'
|
||||
)
|
||||
);
|
||||
|
||||
export const renderEmailChangeEmail = loopback.template(
|
||||
path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'server',
|
||||
'views',
|
||||
'emails',
|
||||
'user-request-update-email.ejs'
|
||||
)
|
||||
);
|
||||
|
||||
export function getWaitPeriod(ttl) {
|
||||
const fiveMinutesAgo = moment().subtract(5, 'minutes');
|
||||
const lastEmailSentAt = moment(new Date(ttl || null));
|
||||
const isWaitPeriodOver = ttl
|
||||
? lastEmailSentAt.isBefore(fiveMinutesAgo)
|
||||
: true;
|
||||
|
||||
if (!isWaitPeriodOver) {
|
||||
const minutesLeft = 5 - (moment().minutes() - lastEmailSentAt.minutes());
|
||||
return minutesLeft;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function getWaitMessage(ttl) {
|
||||
const minutesLeft = getWaitPeriod(ttl);
|
||||
if (minutesLeft <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const timeToWait = minutesLeft
|
||||
? `${minutesLeft} minute${minutesLeft > 1 ? 's' : ''}`
|
||||
: 'a few seconds';
|
||||
|
||||
return dedent`
|
||||
Please wait ${timeToWait} to resend an authentication link.
|
||||
`;
|
||||
}
|
||||
|
||||
export function getEncodedEmail(email) {
|
||||
if (!email) {
|
||||
return null;
|
||||
}
|
||||
return Buffer.from(email).toString('base64');
|
||||
}
|
||||
|
||||
export const decodeEmail = email => Buffer.from(email, 'base64').toString();
|
||||
9
api-server/src/common/utils/constantStrings.json
Normal file
9
api-server/src/common/utils/constantStrings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"aboutUrl": "https://www.freecodecamp.org/about",
|
||||
"defaultProfileImage": "https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png",
|
||||
"donateUrl": "https://www.freecodecamp.org/donate",
|
||||
"forumUrl": "https://forum.freecodecamp.org",
|
||||
"githubUrl": "https://github.com/freecodecamp/freecodecamp",
|
||||
"RSA": "https://forum.freecodecamp.org/t/the-read-search-ask-methodology-for-getting-unstuck/137307",
|
||||
"homeURL": "https://www.freecodecamp.org"
|
||||
}
|
||||
13
api-server/src/common/utils/empty-protector.js
Normal file
13
api-server/src/common/utils/empty-protector.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const emptyProtector = {
|
||||
blocks: [],
|
||||
challenges: []
|
||||
};
|
||||
// protect against malformed map data
|
||||
// protect(block: { challenges: [], block: [] }|Void) => block|emptyProtector
|
||||
export default function protect(block) {
|
||||
// if no block or block has no challenges or blocks
|
||||
if (!block || !(block.challenges || block.blocks)) {
|
||||
return emptyProtector;
|
||||
}
|
||||
return block;
|
||||
}
|
||||
8
api-server/src/common/utils/flash.js
Normal file
8
api-server/src/common/utils/flash.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export const alertTypes = _.keyBy(
|
||||
['success', 'info', 'warning', 'danger'],
|
||||
_.identity
|
||||
);
|
||||
|
||||
export const normalizeAlertType = alertType => alertTypes[alertType] || 'info';
|
||||
21
api-server/src/common/utils/index.js
Normal file
21
api-server/src/common/utils/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { pick } from 'lodash';
|
||||
|
||||
export {
|
||||
getEncodedEmail,
|
||||
decodeEmail,
|
||||
getWaitMessage,
|
||||
getWaitPeriod,
|
||||
renderEmailChangeEmail,
|
||||
renderSignUpEmail,
|
||||
renderSignInEmail
|
||||
} from './auth';
|
||||
|
||||
export const fixCompletedChallengeItem = obj =>
|
||||
pick(obj, [
|
||||
'id',
|
||||
'completedDate',
|
||||
'solution',
|
||||
'githubLink',
|
||||
'challengeType',
|
||||
'files'
|
||||
]);
|
||||
108
api-server/src/common/utils/legacyProjectData.js
Normal file
108
api-server/src/common/utils/legacyProjectData.js
Normal file
@@ -0,0 +1,108 @@
|
||||
const legacyFrontEndProjects = {
|
||||
challenges: [
|
||||
// build-a-personal-portfolio-webpage
|
||||
'bd7158d8c242eddfaeb5bd13',
|
||||
// build-a-random-quote-machine
|
||||
'bd7158d8c442eddfaeb5bd13',
|
||||
// build-a-25-5-clock
|
||||
'bd7158d8c442eddfaeb5bd0f',
|
||||
// build-a-javascript-calculator
|
||||
'bd7158d8c442eddfaeb5bd17',
|
||||
// show-the-local-weather
|
||||
'bd7158d8c442eddfaeb5bd10',
|
||||
// use-the-twitchtv-json-api
|
||||
'bd7158d8c442eddfaeb5bd1f',
|
||||
// stylize-stories-on-camper-news
|
||||
'bd7158d8c442eddfaeb5bd18',
|
||||
// build-a-wikipedia-viewer
|
||||
'bd7158d8c442eddfaeb5bd19',
|
||||
// build-a-tic-tac-toe-game
|
||||
'bd7158d8c442eedfaeb5bd1c',
|
||||
// build-a-simon-game
|
||||
'bd7158d8c442eddfaeb5bd1c'
|
||||
],
|
||||
title: 'Legacy Front End Projects',
|
||||
superBlock: 'legacy-front-end'
|
||||
};
|
||||
|
||||
const legacyBackEndProjects = {
|
||||
challenges: [
|
||||
// timestamp microservice
|
||||
'bd7158d8c443edefaeb5bdef',
|
||||
// request-header-parser-microservice
|
||||
'bd7158d8c443edefaeb5bdff',
|
||||
// url-shortener-microservice
|
||||
'bd7158d8c443edefaeb5bd0e',
|
||||
// image-search-abstraction-layer
|
||||
'bd7158d8c443edefaeb5bdee',
|
||||
// file-metadata-microservice
|
||||
'bd7158d8c443edefaeb5bd0f',
|
||||
// build-a-voting-app
|
||||
'bd7158d8c443eddfaeb5bdef',
|
||||
// build-a-nightlife-coordination-app
|
||||
'bd7158d8c443eddfaeb5bdff',
|
||||
// chart-the-stock-market
|
||||
'bd7158d8c443eddfaeb5bd0e',
|
||||
// manage-a-book-trading-club
|
||||
'bd7158d8c443eddfaeb5bd0f',
|
||||
// build-a-pinterest-clone
|
||||
'bd7158d8c443eddfaeb5bdee'
|
||||
],
|
||||
title: 'Legacy Back End Projects',
|
||||
superBlock: 'legacy-back-end'
|
||||
};
|
||||
|
||||
const legacyDataVisProjects = {
|
||||
challenges: [
|
||||
// build-a-markdown-previewer
|
||||
'bd7157d8c242eddfaeb5bd13',
|
||||
// build-a-camper-leaderboard
|
||||
'bd7156d8c242eddfaeb5bd13',
|
||||
// build-a-recipe-box
|
||||
'bd7155d8c242eddfaeb5bd13',
|
||||
// build-the-game-of-life
|
||||
'bd7154d8c242eddfaeb5bd13',
|
||||
// build-a-roguelike-dungeon-crawler-game
|
||||
'bd7153d8c242eddfaeb5bd13',
|
||||
// visualize-data-with-a-bar-chart
|
||||
'bd7168d8c242eddfaeb5bd13',
|
||||
// visualize-data-with-a-scatterplot-graph
|
||||
'bd7178d8c242eddfaeb5bd13',
|
||||
// visualize-data-with-a-heat-map
|
||||
'bd7188d8c242eddfaeb5bd13',
|
||||
// show-national-contiguity-with-a-force-directed-graph
|
||||
'bd7198d8c242eddfaeb5bd13',
|
||||
// map-data-across-the-globe
|
||||
'bd7108d8c242eddfaeb5bd13'
|
||||
],
|
||||
title: 'Legacy Data Visualization Projects',
|
||||
superBlock: 'legacy-data-visualization'
|
||||
};
|
||||
|
||||
const legacyInfosecQaProjects = {
|
||||
challenges: [
|
||||
// metric-imperial-converter
|
||||
'587d8249367417b2b2512c41',
|
||||
// issue-tracker
|
||||
'587d8249367417b2b2512c42',
|
||||
// personal-library
|
||||
'587d824a367417b2b2512c43',
|
||||
// stock-price-checker
|
||||
'587d824a367417b2b2512c44',
|
||||
// anonymous-message-board
|
||||
'587d824a367417b2b2512c45'
|
||||
],
|
||||
title: 'Legacy Information Security and Quality Assurance Projects',
|
||||
// Keep the settings page "Show Certification" button
|
||||
// pointing to information-security-and-quality-assurance
|
||||
superBlock: 'information-security-and-quality-assurance'
|
||||
};
|
||||
|
||||
const legacyProjects = [
|
||||
legacyFrontEndProjects,
|
||||
legacyBackEndProjects,
|
||||
legacyDataVisProjects,
|
||||
legacyInfosecQaProjects
|
||||
];
|
||||
|
||||
export default legacyProjects;
|
||||
9
api-server/src/common/utils/themes.js
Normal file
9
api-server/src/common/utils/themes.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export const themes = {
|
||||
night: 'night',
|
||||
default: 'default'
|
||||
};
|
||||
|
||||
export const invertTheme = currentTheme =>
|
||||
!currentTheme || currentTheme === themes.default
|
||||
? themes.night
|
||||
: themes.default;
|
||||
Reference in New Issue
Block a user