Merge pull request #29 from wodka/features/api_next

Implement existing API with nestjs
This commit is contained in:
Michael Schramm 2019-09-11 00:09:43 +02:00 committed by GitHub
commit 56a6565427
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
111 changed files with 12194 additions and 35 deletions

2
api/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/node_modules
/yarn.lock

4
api/.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

17
api/Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM node:10 AS builder
MAINTAINER OhMyForm <admin@ohmyform.com>
WORKDIR /usr/src/app
# just copy everhing
COPY . .
RUN yarn install --frozen-lockfile
RUN yarn build
FROM node:10-alpine
COPY --from=builder /usr/src/app /usr/src/app
EXPOSE 3000
CMD [ "yarn", "start:prod" ]

55
api/README.md Normal file
View File

@ -0,0 +1,55 @@
<p align="center">
<a href="http://ohmyform.com/" target="blank"><img src="https://ohmyform.com/img/logo_text.svg" width="320" alt="OhMyForm Logo" /></a>
</p>
## Description
[OhMyForm](https://github.com/ohmyforn) api backend
## Installation
```bash
$ yarn install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# incremental rebuild (webpack)
$ npm run webpack
$ npm run start:hmr
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
TODO
## Stay in touch
- Website - [https://ohmyform.com](https://ohmyform.com/)
## License
TODO

5
api/nest-cli.json Normal file
View File

@ -0,0 +1,5 @@
{
"language": "ts",
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}

6
api/nodemon-debug.json Normal file
View File

@ -0,0 +1,6 @@
{
"watch": ["src"],
"ext": "ts",
"ignore": ["src/**/*.spec.ts"],
"exec": "node --inspect-brk -r ts-node/register src/main.ts"
}

6
api/nodemon.json Normal file
View File

@ -0,0 +1,6 @@
{
"watch": ["src"],
"ext": "ts",
"ignore": ["src/**/*.spec.ts"],
"exec": "ts-node -r tsconfig-paths/register src/main.ts"
}

79
api/package.json Normal file
View File

@ -0,0 +1,79 @@
{
"name": "ohmyform",
"version": "0.1.0",
"description": "Opensource alternative to TypeForm",
"author": "multiple",
"license": "MIT",
"scripts": {
"build": "rimraf dist && tsc -p tsconfig.build.json",
"format": "prettier --write \"src/**/*.ts\"",
"start": "ts-node -r tsconfig-paths/register src/main.ts",
"start:dev": "tsc-watch -p tsconfig.build.json --onSuccess \"node dist/main.js\"",
"start:debug": "tsc-watch -p tsconfig.build.json --onSuccess \"node --inspect-brk dist/main.js\"",
"start:prod": "node dist/main.js",
"lint": "tslint -p tsconfig.json -c tslint.json",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@godaddy/terminus": "^4.1.2",
"@nest-modules/mailer": "^1.1.3",
"@nestjs/common": "^6.5.2",
"@nestjs/core": "^6.5.2",
"@nestjs/jwt": "^6.1.1",
"@nestjs/mongoose": "^6.1.2",
"@nestjs/passport": "^6.1.0",
"@nestjs/platform-express": "^6.5.2",
"@nestjs/swagger": "^3.1.0",
"@nestjs/terminus": "^6.5.0",
"@types/bcrypt": "^3.0.0",
"@types/mongoose": "^5.5.11",
"bcrypt": "^3.0.6",
"class-transformer": "^0.2.3",
"class-validator": "^0.9.1",
"handlebars": "^4.1.2",
"mongoose": "^5.6.7",
"nestjs-typegoose": "^5.2.1",
"passport": "^0.4.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"reflect-metadata": "^0.1.12",
"rimraf": "^2.6.2",
"rxjs": "^6.3.3",
"swagger-ui-express": "^4.0.7",
"typegoose": "^5.9.0"
},
"devDependencies": {
"@nestjs/testing": "6.1.1",
"@types/express": "4.16.1",
"@types/jest": "24.0.11",
"@types/node": "11.13.4",
"@types/supertest": "2.0.7",
"jest": "24.7.1",
"prettier": "1.17.0",
"supertest": "4.0.2",
"ts-jest": "24.0.2",
"ts-node": "8.1.0",
"tsc-watch": "2.2.1",
"tsconfig-paths": "3.8.0",
"tslint": "5.16.0",
"typescript": "3.4.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

20
api/src/app.controller.ts Normal file
View File

@ -0,0 +1,20 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import {ApiExcludeEndpoint} from "@nestjs/swagger"
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@ApiExcludeEndpoint()
@Get()
getIndex(): object {
return {
message: 'hello humanoid',
_links: {
doc: '/doc',
health: '/health'
}
};
}
}

35
api/src/app.module.ts Normal file
View File

@ -0,0 +1,35 @@
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { MongooseModule } from '@nestjs/mongoose';
import { TypegooseModule } from 'nestjs-typegoose';
import { TerminusOptionsService } from './terminus-options.service';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from "./user/user.module"
import { FormModule } from "./form/form.module"
import { AuthModule } from './auth/auth.module';
import { MailModule } from "./mail/mail.module"
import { MailerModule } from "@nest-modules/mailer"
@Module({
imports: [
TypegooseModule.forRoot('mongodb://localhost/ohmyform', { useNewUrlParser: true }),
MongooseModule.forRoot('mongodb://localhost/ohmyform'),
TerminusModule.forRootAsync({
useClass: TerminusOptionsService,
}),
MailerModule.forRoot({
transport: 'smtp://localhost:1025',
defaults: {
from:'"OhMyForm" <noreply@ohmyform.com>',
}
}),
UserModule,
FormModule,
AuthModule,
MailModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

8
api/src/app.service.ts Normal file
View File

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@ -0,0 +1,9 @@
import {AuthController} from "./controllers/auth.controller"
import {RecoverController} from "./controllers/recover.controller"
import {RegisterController} from "./controllers/register.controller"
export default [
AuthController,
RecoverController,
RegisterController,
]

View File

@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { UserModule } from '../user/user.module';
import { PassportModule } from '@nestjs/passport';
import { jwtConstants } from "./constants"
import { JwtModule } from "@nestjs/jwt"
import controllers from './auth.controllers'
import providers from './auth.providers'
import { MailModule } from "../mail/mail.module"
@Module({
imports: [
UserModule,
PassportModule,
MailModule,
JwtModule.register({
secret: jwtConstants.secret,
signOptions: { expiresIn: '12h' },
}),
],
controllers,
providers
})
export class AuthModule {}

View File

@ -0,0 +1,15 @@
import { AuthService } from "./services/auth.service"
import { PasswordService } from "./services/password.service"
import { PasswordStrategy } from "./strategies/password.strategy"
import { JwtStrategy } from "./strategies/jwt.strategy"
import { JwtRefreshStrategy } from "./strategies/jwt.refresh.strategy"
import { RegisterService } from "./services/register.service"
export default [
AuthService,
PasswordService,
PasswordStrategy,
JwtStrategy,
JwtRefreshStrategy,
RegisterService,
]

View File

@ -0,0 +1,3 @@
export const jwtConstants = {
secret: 'secretKey',
};

View File

@ -0,0 +1,30 @@
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from "../services/auth.service"
import { AuthJwtDto } from "../dto/auth.jwt.dto"
import {ApiBearerAuth, ApiImplicitBody, ApiImplicitQuery, ApiResponse, ApiUseTags} from "@nestjs/swagger"
@ApiUseTags('authentication')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@ApiResponse({ status: 201, description: 'Successful login.', type: AuthJwtDto})
@ApiResponse({ status: 401, description: 'Invalid Credentials.'})
@ApiImplicitQuery({name: 'username', type: String})
@ApiImplicitQuery({name: 'password', type: String})
@UseGuards(AuthGuard('password'))
@Post('login')
async login(@Request() req): Promise<AuthJwtDto> {
return this.authService.createToken(req.user);
}
@ApiBearerAuth()
@ApiResponse({ status: 201, description: 'Consumed Refresh Token.', type: AuthJwtDto})
@ApiResponse({ status: 401, description: 'Invalid Token.'})
@UseGuards(AuthGuard('jwt.refresh'))
@Post('refresh')
async refresh(@Request() req): Promise<AuthJwtDto> {
return this.authService.createToken(req.user);
}
}

View File

@ -0,0 +1,29 @@
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from "../services/auth.service"
import { AuthJwtDto } from "../dto/auth.jwt.dto"
import {ApiBearerAuth, ApiImplicitBody, ApiImplicitQuery, ApiResponse, ApiUseTags} from "@nestjs/swagger"
@ApiUseTags('authentication')
@Controller('auth/recover')
export class RecoverController {
constructor(private readonly authService: AuthService) {}
@ApiResponse({ status: 201, description: 'Successful registration.', type: AuthJwtDto})
@ApiImplicitQuery({name: 'email', type: String})
@ApiImplicitQuery({name: 'username', type: String})
@Post('request')
async request(@Request() req): Promise<AuthJwtDto> {
// TODO
return null
}
@ApiResponse({ status: 201, description: 'Successful registration.', type: AuthJwtDto})
@ApiImplicitQuery({name: 'token', type: String})
@ApiImplicitQuery({name: 'password', type: String})
@Post('finish')
async finish(@Request() req): Promise<AuthJwtDto> {
// TODO
return null
}
}

View File

@ -0,0 +1,18 @@
import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from "../services/auth.service"
import { ApiBadRequestResponse, ApiCreatedResponse, ApiUseTags } from "@nestjs/swagger"
import { RegisterDto } from "../dto/register.dto"
import {RegisterService} from "../services/register.service"
@ApiUseTags('authentication')
@Controller('auth')
export class RegisterController {
constructor(private readonly registerService: RegisterService) {}
@ApiCreatedResponse({ description: 'Successful registration.'})
@ApiBadRequestResponse({})
@Post('register')
async register(@Body() params: RegisterDto): Promise<void> {
await this.registerService.register(params.username, params.email, params.password)
}
}

View File

@ -0,0 +1,9 @@
import { ApiModelProperty } from '@nestjs/swagger';
export class AuthJwtDto {
@ApiModelProperty()
accessToken: string;
@ApiModelProperty()
refreshToken: string;
}

View File

@ -0,0 +1,20 @@
import { ApiModelProperty } from "@nestjs/swagger"
import { IsEmail, IsNotEmpty, Validate } from "class-validator"
import { UsernameAlreadyInUse } from "../../user/validators/UsernameAlreadyInUse"
import { EmailAlreadyInUse } from "../../user/validators/EmailAlreadyInUse"
export class RegisterDto {
@ApiModelProperty()
@IsNotEmpty()
@Validate(UsernameAlreadyInUse)
readonly username: string;
@ApiModelProperty()
@IsNotEmpty()
readonly password: string;
@ApiModelProperty()
@Validate(EmailAlreadyInUse)
@IsEmail()
readonly email: string;
}

View File

@ -0,0 +1,8 @@
export interface AuthUser {
id: string;
username: string;
email: string;
roles: [string];
created: Date;
lastUpdated: Date;
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile();
service = module.get<AuthService>(AuthService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,70 @@
import { Injectable } from '@nestjs/common';
import { UserService } from '../../user/services/user.service';
import { PasswordService } from "./password.service"
import { JwtService } from '@nestjs/jwt';
import { AuthUser } from "../interfaces/auth.user.interface"
import { User } from "../../user/models/user.model"
import { AuthJwtDto } from "../dto/auth.jwt.dto"
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UserService,
private readonly passwordService: PasswordService,
private readonly jwtService: JwtService
) {}
async verifyForLogin(identifier: string, pass: string): Promise<AuthUser | null> {
try {
const user = await this.usersService.findOneByIdentifier(identifier);
if (await this.passwordService.verify(pass, user.passwordHash, user.salt)) {
return AuthService.setupAuthUser(user)
}
} catch (e) {}
return null;
}
async validate(identifier: string): Promise<AuthUser | null> {
try {
return AuthService.setupAuthUser(
await this.usersService.findOneByIdentifier(identifier)
)
} catch (e) {}
return null;
}
private static setupAuthUser(user:User):AuthUser {
return {
id: user._id,
username: user.username,
email: user.email,
roles: user.roles,
created: user.created,
lastUpdated: user.lastModified
};
}
async createToken(user: AuthUser): Promise<AuthJwtDto> {
const payload = {
id: user.id,
username: user.username,
scope: user.roles,
};
return {
accessToken: this.jwtService.sign(payload),
// TODO add refresh token invalidation uppon usage! They should only work once
refreshToken: this.jwtService.sign(
{
...payload,
refresh: true
},
{
expiresIn: '30days',
}
),
};
}
}

View File

@ -0,0 +1,30 @@
import { Injectable } from "@nestjs/common"
import * as crypto from 'crypto'
import * as bcrypt from 'bcrypt'
@Injectable()
export class PasswordService {
async verify (password: string, hash: string, salt?: string): Promise<boolean> {
console.log('try to verify password', password)
if (password[0] === '$') {
return await bcrypt.compare(password, hash)
}
//Generate salt if it doesn't exist yet
if(!salt){
salt = crypto.randomBytes(64).toString('base64');
}
return hash === crypto.pbkdf2Sync(
password,
new Buffer(salt, 'base64'),
10000,
128,
'SHA1'
).toString('base64');
}
hash (password): Promise<string> {
return bcrypt.hash(password, 4)
}
}

View File

@ -0,0 +1,33 @@
import { Injectable } from "@nestjs/common"
import { MailService } from "../../mail/services/mail.service"
import { User } from "../../user/models/user.model"
import { PasswordService } from "./password.service"
import { UserService } from "../../user/services/user.service"
@Injectable()
export class RegisterService {
constructor(
private readonly mailService: MailService,
private readonly passwordService: PasswordService,
private readonly userService: UserService
) {}
async register (username: string, email: string, password: string): Promise<void> {
let user = new User()
user.email = email
user.username = username
user.passwordHash = await this.passwordService.hash(password)
await this.userService.save(user)
await this.mailService.sendEmail(
{
template: 'auth/register',
to: email
},
{
confirm: 'some url'
}
)
}
}

View File

@ -0,0 +1,26 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from '../constants';
import { AuthService } from "../services/auth.service"
import { AuthUser } from "../interfaces/auth.user.interface"
@Injectable()
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt.refresh') {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any): Promise<AuthUser | null> {
if (!payload.refresh) {
return null
}
// TODO add refresh token invalidation uppon usage! They should only work once
return this.authService.validate(payload.username);
}
}

View File

@ -0,0 +1,21 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from '../constants';
import { AuthService } from "../services/auth.service"
import {AuthUser} from "../interfaces/auth.user.interface"
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: jwtConstants.secret,
});
}
async validate(payload: any): Promise<AuthUser | null> {
return this.authService.validate(payload.username);
}
}

View File

@ -0,0 +1,16 @@
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { AuthService } from '../services/auth.service';
import {AuthUser} from "../interfaces/auth.user.interface"
@Injectable()
export class PasswordStrategy extends PassportStrategy(Strategy, 'password') {
constructor(private readonly authService: AuthService) {
super();
}
validate(username: string, password: string): Promise<AuthUser | null> {
return this.authService.verifyForLogin(username, password);
}
}

View File

@ -0,0 +1,6 @@
import { IsMongoId } from 'class-validator';
export class FindOneDto {
@IsMongoId()
id: string;
}

View File

@ -0,0 +1,47 @@
import {Controller, Request, Get, Post, Put, Delete, UseGuards, Param, NotImplementedException} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth, ApiImplicitQuery, ApiResponse, ApiUseTags } from "@nestjs/swagger"
import { FormService } from "../services/form.service"
import { FormDto } from "../dto/form.dto"
import { FindOneDto } from "../../core/dto/find.one.dto"
@ApiUseTags('forms')
@ApiBearerAuth()
@Controller('forms')
export class FormController {
constructor(private readonly formService: FormService) {}
@Get()
@UseGuards(AuthGuard('jwt'))
async list(@Request() req): Promise<FormDto[]> {
// TODO calculate total forms, add for pagination
const results = await this.formService.findBy({})
return results.map(form => new FormDto(form))
}
@Post()
@UseGuards(AuthGuard('jwt'))
async create(@Request() req): Promise<FormDto> {
throw new NotImplementedException()
}
@ApiResponse({ status: 200, description: 'Form Object', type: FormDto})
@ApiImplicitQuery({name: 'id', type: String})
@Get(':id')
@UseGuards(AuthGuard('jwt'))
async read(@Param() params: FindOneDto): Promise<FormDto> {
return new FormDto(await this.formService.findById(params.id));
}
@Put(':id')
@UseGuards(AuthGuard('jwt'))
async update(@Param() params: FindOneDto, @Request() req): Promise<FormDto> {
throw new NotImplementedException()
}
@Delete(':id')
@UseGuards(AuthGuard('jwt'))
async delete(@Param() params: FindOneDto): Promise<void> {
throw new NotImplementedException()
}
}

View File

@ -0,0 +1,25 @@
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
import { ApiImplicitQuery, ApiResponse, ApiUseTags } from "@nestjs/swagger"
import { FormService } from "../services/form.service"
import { Form } from "../models/form.model"
import { PublicFormDto } from "../dto/public.form.dto"
import { FindOneDto } from "../../core/dto/find.one.dto"
@ApiUseTags('forms')
@Controller('public')
export class PublicController {
constructor(private readonly formService: FormService) {}
@ApiResponse({ status: 200, description: 'Form Object', type: PublicFormDto})
@ApiImplicitQuery({name: 'id', type: String})
@Get(':id')
async read(@Param() params: FindOneDto): Promise<PublicFormDto> {
const form:Form = await this.formService.findById(params.id)
if (!form.isLive) {
throw new NotFoundException();
}
return new PublicFormDto(form);
}
}

View File

@ -0,0 +1,39 @@
import { ApiModelProperty } from '@nestjs/swagger';
import { Form } from "../models/form.model"
export class FormDto {
@ApiModelProperty()
id: string;
@ApiModelProperty()
title: string;
@ApiModelProperty()
live: boolean;
@ApiModelProperty()
created: Date;
@ApiModelProperty()
lastModified: Date;
@ApiModelProperty()
fields: any;
@ApiModelProperty()
info: {
responses: number;
}
constructor(partial: Partial<Form>) {
this.id = partial._id.toString()
this.title = partial.title
this.live = partial.isLive
this.created = partial.created
this.lastModified = partial.lastModified
this.fields = partial.form_fields
this.info = {
responses: 0 // TODO
}
}
}

View File

@ -0,0 +1,17 @@
import { ApiModelProperty } from '@nestjs/swagger';
import { Form } from "../models/form.model"
export class PublicFormDto {
@ApiModelProperty()
id: string;
@ApiModelProperty()
title: string;
fields: [];
constructor(partial: Partial<Form>) {
this.id = partial._id.toString();
this.title = partial.title
}
}

View File

@ -0,0 +1,7 @@
import { FormController } from "./controllers/form.controller"
import { PublicController } from "./controllers/public.controller"
export default [
FormController,
PublicController,
]

View File

@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import controllers from './form.controllers';
import providers from './form.providers'
import {TypegooseModule} from "nestjs-typegoose"
import {Form} from "./models/form.model"
@Module({
imports: [
TypegooseModule.forFeature([Form]),
],
exports: [],
controllers,
providers,
})
export class FormModule {}

View File

@ -0,0 +1,5 @@
import { FormService } from "./services/form.service"
export default [
FormService
]

View File

@ -0,0 +1,5 @@
import { Document } from 'mongoose';
export interface Button extends Document{
}

View File

@ -0,0 +1,5 @@
import { Document } from 'mongoose';
export interface Field extends Document{
}

View File

@ -0,0 +1,51 @@
import { Document } from 'mongoose';
import { Field } from "./field.interface"
import { Button } from "./button.interface"
export class Form extends Document{
readonly firstName: string;
readonly language: string;
readonly analytics: string;
readonly form_fields: Field[];
readonly admin: any;
readonly startPage: {
readonly showStart: boolean;
readonly introTitle: string;
readonly introParagraph: string;
readonly introButtonText: string;
readonly buttons: Button[];
};
readonly endPage: {
readonly showEnd: boolean;
readonly title: string;
readonly paragraph: string;
readonly buttonText: string;
readonly buttons: Button[];
};
readonly selfNotifications: {
readonly fromField: string;
readonly toEmails: string;
readonly subject: string;
readonly htmlTemplate: string;
readonly enabled: boolean;
};
readonly respondentNotifications: {
readonly toField: string;
readonly fromEmails: string;
readonly subject: string;
readonly htmlTemplate: string;
readonly enabled: boolean;
};
readonly showFooter: boolean;
readonly isLive: boolean;
readonly design: {
readonly colors: {
readonly backgroundColor: string;
readonly questionColor: string;
readonly answerColor: string;
readonly buttonColor: string;
readonly buttonTextColor: string;
};
readonly font: string;
};
}

View File

@ -0,0 +1,14 @@
import {prop} from "typegoose"
import {VisitorData} from "./visitor.data"
import {Exclude} from "class-transformer"
export class Analytics {
@Exclude()
readonly _id: string;
@prop()
gaCode: string;
@prop()
visitors: VisitorData[]
}

View File

@ -0,0 +1,30 @@
import {Exclude} from "class-transformer"
import {prop} from "typegoose"
export class Button {
@Exclude()
readonly _id: string;
@prop({
match: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/
})
url: string;
@prop()
action: string;
@prop()
text: string;
@prop({
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
default: '#5bc0de'
})
bgColor: string;
@prop({
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
default: '#ffffff'
})
color: string;
}

View File

@ -0,0 +1,37 @@
import {prop, Ref, Typegoose} from "typegoose"
import {Exclude} from "class-transformer"
export class Colors {
@Exclude()
readonly _id: string;
@prop({
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
default: '#fff'
})
readonly backgroundColor: string;
@prop({
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
default: '#333'
})
readonly questionColor: string;
@prop({
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
default: '#333'
})
readonly answerColor: string;
@prop({
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
default: '#fff'
})
readonly buttonColor: string;
@prop({
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
default: '#333'
})
readonly buttonTextColor: string;
}

View File

@ -0,0 +1,14 @@
import {prop} from "typegoose"
import {Colors} from "./colors"
import {Exclude} from "class-transformer"
export class Design {
@Exclude()
readonly _id: string;
@prop()
readonly colors: Colors;
@prop()
readonly font: string;
}

View File

@ -0,0 +1,29 @@
import {prop} from "typegoose"
import {Button} from "./button"
import {Exclude} from "class-transformer"
export class EndPage {
@Exclude()
readonly _id: string;
@prop({
default: false
})
readonly showEnd: boolean;
@prop({
default: 'Thank you for filling out the form'
})
readonly title: string;
@prop()
readonly paragraph: string;
@prop({
default: 'Go back to Form'
})
readonly buttonText: string;
@prop()
readonly buttons: Button[];
}

View File

@ -0,0 +1,19 @@
import {prop} from "typegoose"
import {VisitorData} from "./visitor.data"
import {Exclude} from "class-transformer"
export class FieldOption {
@Exclude()
readonly _id: string;
@prop()
option_id: number;
@prop()
option_title: string;
@prop({
trim: true
})
option_value: string;
}

View File

@ -0,0 +1,83 @@
import {arrayProp, prop} from "typegoose"
import {LogicJump} from "./logic.jump"
import {RatingField} from "./rating.field"
import {FieldOption} from "./field.option"
import {Exclude} from "class-transformer"
export class Field {
@Exclude()
readonly _id: string;
@prop({
default: false
})
isSubmission: boolean;
@prop()
submissionId: string;
@prop({
trim: true
})
title: string;
@prop({
default: ''
})
description: string;
@prop()
logicJump: LogicJump;
@prop()
ratingOptions: RatingField;
@arrayProp({
items: FieldOption
})
fieldOptions: FieldOption[];
@prop({
default: true
})
required: boolean;
@prop({
default: false
})
disabled: boolean;
@prop({
default: false
})
deletePreserved: boolean;
@arrayProp({
items: String
})
validFieldTypes: boolean;
@prop({
enum: [
'textfield',
'date',
'email',
'legal',
'textarea',
'link',
'statement',
'dropdown',
'rating',
'radio',
'hidden',
'yes_no',
'number'
]
})
fieldType: string;
@prop({
default: ''
})
fieldValue: any;
}

View File

@ -0,0 +1,45 @@
import {prop, Ref} from "typegoose"
import {VisitorData} from "./visitor.data"
import {Exclude} from "class-transformer"
import {Field} from "./field"
export class LogicJump {
@Exclude()
readonly _id: string;
@prop({
enum: [
'field == static',
'field != static',
'field > static',
'field >= static',
'field <= static',
'field < static',
'field contains static',
'field !contains static',
'field begins static',
'field !begins static',
'field ends static',
'field !ends static'
]
})
expressionString: string;
@prop({
ref: Field
})
fieldA: Ref<Field>;
@prop()
valueB: string;
@prop({
ref: Field
})
jumpTo: Ref<Field>;
@prop({
default: false
})
enabled: boolean;
}

View File

@ -0,0 +1,38 @@
import {arrayProp, prop} from "typegoose"
import {VisitorData} from "./visitor.data"
import {Exclude} from "class-transformer"
export class RatingField {
@Exclude()
readonly _id: string;
@prop({
min: 1,
max: 10
})
steps: number;
@prop({
enum: [
'Heart',
'Star',
'thumbs-up',
'thumbs-down',
'Circle',
'Square',
'Check Circle',
'Smile Outlined',
'Hourglass',
'bell',
'Paper Plane',
'Comment',
'Trash'
]
})
shape: string;
@arrayProp({
items: String
})
validShapes: [string];
}

View File

@ -0,0 +1,30 @@
import {prop} from "typegoose"
import {Exclude} from "class-transformer"
export class RespondentNotifications {
@Exclude()
readonly _id: string;
@prop()
readonly toField: string;
@prop({
match: [/.+\@.+\..+/, 'Please fill a valid email address']
})
readonly fromEmails: string;
@prop({
default: 'OhMyForm: Thank you for filling out this OhMyForm'
})
readonly subject: string;
@prop({
default: 'Hello, <br><br> Weve received your submission. <br><br> Thank you & have a nice day!',
})
readonly htmlTemplate: string;
@prop({
default: false
})
readonly enabled: boolean;
}

View File

@ -0,0 +1,24 @@
import {prop} from "typegoose"
import {Exclude} from "class-transformer"
export class SelfNotifications {
@Exclude()
readonly _id: string;
@prop()
readonly fromField: string;
@prop()
readonly toEmails: string;
@prop()
readonly subject: string;
@prop()
readonly htmlTemplate: string;
@prop({
default: false
})
readonly enabled: boolean;
}

View File

@ -0,0 +1,29 @@
import {prop} from "typegoose"
import {Button} from "./button"
import {Exclude} from "class-transformer"
export class StartPage {
@Exclude()
readonly _id: string;
@prop({
default: false
})
readonly showStart: boolean;
@prop({
default: 'Welcome to Form'
})
readonly introTitle: string;
@prop()
readonly introParagraph: string;
@prop({
default: 'Start'
})
readonly introButtonText: string;
@prop()
readonly buttons: Button[];
}

View File

@ -0,0 +1,44 @@
import {Exclude} from "class-transformer"
import {arrayProp, prop} from "typegoose"
import {Field} from "./field"
import {Ref} from "typegoose/lib/prop"
export class VisitorData {
@Exclude()
readonly _id: string;
@prop()
readonly introParagraph: string;
@prop()
readonly referrer: string;
@arrayProp({
itemsRef: Field
})
readonly filledOutFields: Ref<Field>[];
@prop()
readonly timeElapsed: number;
@prop()
readonly isSubmitted: boolean;
@prop({
enum: ['en', 'fr', 'es', 'it', 'de'],
default: 'en',
})
readonly language: string;
@prop()
readonly ipAddr: string;
@prop({
enum: ['desktop', 'phone', 'tablet', 'other'],
default: 'other'
})
readonly deviceType: string;
@prop()
readonly userAgent: string;
}

View File

@ -0,0 +1,72 @@
import {arrayProp, prop, Ref, Typegoose} from "typegoose"
import {Analytics} from "./embedded/analytics"
import {Field} from "./embedded/field"
import {StartPage} from "./embedded/start.page"
import {EndPage} from "./embedded/end.page"
import {SelfNotifications} from "./embedded/self.notifications"
import {RespondentNotifications} from "./embedded/respondent.notifications"
import {Design} from "./embedded/design"
import {User} from "../../user/models/user.model"
export class Form extends Typegoose {
readonly _id: any;
@prop({
trim: true,
required: 'Form Title cannot be blank'
})
readonly title: string;
@prop()
readonly created: any;
@prop()
readonly lastModified: any;
@prop({
enum: ['en', 'fr', 'es', 'it', 'de'],
default: 'en',
required: 'Form must have a language'
})
readonly language: string;
@prop()
readonly analytics: Analytics;
@arrayProp({
items: Field,
default: []
})
readonly form_fields: Field[];
@prop({
ref: User,
required: 'Form must have an Admin'
})
readonly admin: Ref<User>;
@prop()
readonly startPage: StartPage;
@prop()
readonly endPage: EndPage;
@prop()
readonly selfNotifications: SelfNotifications;
@prop()
readonly respondentNotifications: RespondentNotifications;
@prop({
default: true
})
readonly showFooter: boolean;
@prop({
default: true
})
readonly isLive: boolean;
@prop()
readonly design: Design;
}

View File

@ -0,0 +1,18 @@
import {Injectable} from '@nestjs/common';
import { InjectModel } from 'nestjs-typegoose';
import { ModelType } from 'typegoose';
import {Form} from "../models/form.model"
import {User} from "../../user/models/user.model"
@Injectable()
export class FormService {
constructor(@InjectModel(Form) private readonly formModel: ModelType<Form>) {}
async findById(id: string): Promise<Form> {
return await this.formModel.findById(id).exec()
}
async findBy(conditions): Promise<Form[]> {
return await this.formModel.find(conditions).exec()
}
}

View File

@ -0,0 +1,12 @@
export class OptionsDto {
template: string;
language?: string;
to: string;
cc?: string[];
bcc?: string[];
}

View File

@ -0,0 +1,5 @@
import { MailService } from "./services/mail.service"
export default [
MailService
]

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import providers from './mail.providers'
import exportList from './mail.exports'
import { HandlebarsAdapter, MailerModule } from "@nest-modules/mailer"
@Module({
imports: [
],
providers,
exports: exportList,
})
export class MailModule {}

View File

@ -0,0 +1,5 @@
import { MailService } from "./services/mail.service"
export default [
MailService
]

View File

@ -0,0 +1,35 @@
import {Inject, Injectable, NotFoundException} from '@nestjs/common';
import { OptionsDto } from "../dto/options.dto"
import { MailerService } from '@nest-modules/mailer';
import * as Handlebars from 'handlebars'
import * as fs from 'fs';
@Injectable()
export class MailService {
constructor(
private readonly nodeMailer: MailerService
) {}
async sendEmail(options:OptionsDto, placeholders:any): Promise<boolean> {
const template = fs.readFileSync(`${__dirname}/../../../views/en/mail/${options.template}.hbs`, 'UTF-8');
const parts = Handlebars
.compile(template)
(placeholders)
.split('---')
const mail:any = {
to: options.to,
subject: parts[0],
text: parts[1],
}
if (parts.length > 2) {
mail.html = parts[2]
}
await this.nodeMailer.sendMail(mail)
return false
}
}

33
api/src/main.ts Normal file
View File

@ -0,0 +1,33 @@
import {NestFactory, Reflector} from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';
import { useContainer } from "class-validator"
const pkg = require('../package.json')
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true });
useContainer(app.select(AppModule), { fallbackOnErrors: true });
app.useGlobalPipes(new ValidationPipe({
disableErrorMessages: false,
transform: true,
}));
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)))
const options = new DocumentBuilder()
.setTitle('OhMyForm')
.setDescription('API documentation')
.setVersion(pkg.version)
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('doc', app, document);
await app.listen(3000);
}
bootstrap();

View File

@ -0,0 +1,29 @@
import {
TerminusEndpoint,
TerminusOptionsFactory,
DNSHealthIndicator,
MongooseHealthIndicator,
TerminusModuleOptions
} from '@nestjs/terminus';
import { Injectable } from '@nestjs/common';
@Injectable()
export class TerminusOptionsService implements TerminusOptionsFactory {
constructor(
private readonly dns: DNSHealthIndicator,
private readonly mongoose: MongooseHealthIndicator
) {}
createTerminusOptions(): TerminusModuleOptions {
const healthEndpoint: TerminusEndpoint = {
url: '/health',
healthIndicators: [
async () => this.dns.pingCheck('mail', 'https://google.com'),
async () => this.mongoose.pingCheck('mongo')
],
};
return {
endpoints: [healthEndpoint],
};
}
}

View File

@ -0,0 +1,53 @@
import {Controller, Request, Get, Post, Put, Delete, UseGuards, Param, NotImplementedException} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth, ApiImplicitQuery, ApiResponse, ApiUseTags } from "@nestjs/swagger"
import { UserService } from "../services/user.service"
import { UserDto } from "../dto/user.dto"
import { FindOneDto } from "../../core/dto/find.one.dto"
@ApiUseTags('users')
@ApiBearerAuth()
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
@UseGuards(AuthGuard('jwt'))
async list(@Request() req): Promise<UserDto[]> {
// TODO calculate total forms, add for pagination
const results = await this.userService.findBy({})
return results.map(form => new UserDto(form))
}
@ApiResponse({ status: 200, description: 'User Object', type: UserDto})
@Post()
@UseGuards(AuthGuard('jwt'))
async create(@Request() req): Promise<UserDto> {
throw new NotImplementedException()
}
@ApiResponse({ status: 200, description: 'User Object', type: UserDto})
@ApiImplicitQuery({name: 'id', type: String})
@Get(':id')
@UseGuards(AuthGuard('jwt'))
async read(@Param() params: FindOneDto): Promise<UserDto> {
return new UserDto(await this.userService.findById(params.id));
}
@ApiResponse({ status: 200, description: 'User Object', type: UserDto})
@ApiImplicitQuery({name: 'id', type: String})
@Put(':id')
@UseGuards(AuthGuard('jwt'))
async update(@Param() params: FindOneDto, @Request() req): Promise<UserDto> {
throw new NotImplementedException()
}
@ApiResponse({ status: 200, description: 'User Object', type: UserDto})
@ApiImplicitQuery({name: 'id', type: String})
@Delete(':id')
@UseGuards(AuthGuard('jwt'))
async delete(@Param() params: FindOneDto): Promise<void> {
throw new NotImplementedException()
}
}

View File

@ -0,0 +1,27 @@
import { ApiModelProperty } from '@nestjs/swagger';
import {User} from "../models/user.model"
export class UserDto {
@ApiModelProperty()
id: string;
@ApiModelProperty()
username: string;
@ApiModelProperty()
email: string;
@ApiModelProperty()
roles: string[];
@ApiModelProperty()
created: Date;
constructor(partial: Partial<User>) {
this.id = partial._id.toString()
this.username = partial.username
this.email = partial.email
this.roles = partial.roles
this.created = partial.created
}
}

View File

@ -0,0 +1,97 @@
import {arrayProp, pre, prop, Typegoose} from 'typegoose';
import { IsString } from 'class-validator';
import {Exclude} from "class-transformer"
@pre<User>('save', (next) => {
this.lastModified = new Date()
next()
})
export class User extends Typegoose {
@Exclude()
readonly _id: string;
@prop({
trim: true,
default: ''
})
firstName: string;
@prop({
trim: true,
default: ''
})
lastName: string;
@prop({
trim: true,
lowercase: true,
unique: true, // 'Account already exists with this email',
match: [
/^(([^<>()\[\]\\.,;:\s@']+(\.[^<>()\[\]\\.,;:\s@']+)*)|('.+'))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
'Please fill a valid email address'
],
required: [true, 'Email is required']
})
email: string;
@prop({
unique: true,
lowercase: true,
match: [
/^[a-zA-Z0-9\-]+$/,
'Username can only contain alphanumeric characters and \'-\''
],
required: [true, 'Username is required']
})
username: string;
@prop({
default: ''
})
passwordHash: string;
@prop()
salt: string;
@prop({
default: 'local'
})
provider: string;
@arrayProp({
items: String,
enum: ['user', 'admin', 'superuser'],
default: ['user']
})
roles: [string];
@prop({
enum: ['en', 'fr', 'es', 'it', 'de'],
default: 'en',
})
language: string;
@prop({
default: Date.now
})
readonly created: Date;
@prop()
readonly lastModified: Date;
@prop()
resetPasswordToken: string;
@prop()
resetPasswordExpires: Date;
@prop()
token: string;
@prop({
unique: true,
index: true,
sparse: true
})
apiKey: string;
}

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './users.service';
describe('UsersService', () => {
let service: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserService],
}).compile();
service = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,44 @@
import {Injectable, NotFoundException} from '@nestjs/common';
import { InjectModel } from 'nestjs-typegoose';
import { ModelType } from 'typegoose';
import { User } from "../models/user.model"
import {Form} from "../../form/models/form.model"
@Injectable()
export class UserService {
constructor(@InjectModel(User) private readonly userModel: ModelType<User>) {}
async findOneByIdentifier(identifier: string): Promise<User> {
const results = await this.userModel.find().or([
{
username: identifier
},
{
email: identifier
}
]).exec();
if (results.length !== 1) {
throw new NotFoundException()
}
return results[0]
}
async findById(id: string): Promise<User> {
return await this.userModel.findById(id).exec()
}
async findOneBy(conditions): Promise<User> {
return await this.userModel.findOne(conditions).exec()
}
async findBy(conditions): Promise<User[]> {
return await this.userModel.find(conditions).exec()
}
async save(user: User): Promise<User> {
let model = new this.userModel(user)
return await model.save()
}
}

View File

@ -0,0 +1,5 @@
import { UserController } from "./controllers/user.controller"
export default [
UserController,
]

View File

@ -0,0 +1,9 @@
import { UserService } from "./services/user.service"
import { UsernameAlreadyInUse } from "./validators/UsernameAlreadyInUse"
import { EmailAlreadyInUse } from "./validators/EmailAlreadyInUse"
export default [
UserService,
UsernameAlreadyInUse,
EmailAlreadyInUse,
]

View File

@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { TypegooseModule } from 'nestjs-typegoose';
import providers from './user.providers'
import exportList from './user.exports'
import { User } from "./models/user.model"
import controllers from './user.controllers'
@Module({
imports: [
TypegooseModule.forFeature([User]),
],
controllers,
providers,
exports: exportList,
})
export class UserModule {}

View File

@ -0,0 +1,9 @@
import { UserService } from "./services/user.service"
import { UsernameAlreadyInUse } from "./validators/UsernameAlreadyInUse"
import { EmailAlreadyInUse } from "./validators/EmailAlreadyInUse"
export default [
UserService,
UsernameAlreadyInUse,
EmailAlreadyInUse,
]

View File

@ -0,0 +1,20 @@
import { ValidationArguments, ValidatorConstraint } from "class-validator"
import { Inject, Injectable } from "@nestjs/common"
import { UserService } from "../services/user.service"
@ValidatorConstraint({ name: 'EmailAlreadyInUse', async: true })
@Injectable()
export class EmailAlreadyInUse {
constructor(
@Inject('UserService') private readonly userService: UserService,
) {}
async validate(text: string) {
const user = await this.userService.findOneBy({email: text});
return !user;
}
defaultMessage(args: ValidationArguments) {
return 'User with this email already exists.';
}
}

View File

@ -0,0 +1,20 @@
import { ValidationArguments, ValidatorConstraint } from "class-validator"
import { Inject, Injectable } from "@nestjs/common"
import { UserService } from "../services/user.service"
@ValidatorConstraint({ name: 'UsernameAlreadyInUse', async: true })
@Injectable()
export class UsernameAlreadyInUse {
constructor(
@Inject('UserService') private readonly userService: UserService,
) {}
async validate(text: string) {
const user = await this.userService.findOneBy({username: text});
return !user;
}
defaultMessage(args: ValidationArguments) {
return 'User with this username already exists.';
}
}

4
api/tsconfig.build.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

15
api/tsconfig.json Normal file
View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true
},
"exclude": ["node_modules", "dist"]
}

59
api/tslint.json Normal file
View File

@ -0,0 +1,59 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {
"no-unused-expression": true
},
"rules": {
"eofline": false,
"quotemark": [
true,
"single"
],
"indent": false,
"member-access": [
false
],
"ordered-imports": [
false
],
"max-line-length": [
true,
150
],
"member-ordering": [
false
],
"curly": false,
"interface-name": [
false
],
"array-type": [
false
],
"no-empty-interface": false,
"no-empty": false,
"arrow-parens": false,
"object-literal-sort-keys": false,
"no-unused-expression": false,
"max-classes-per-file": [
false
],
"variable-name": [
false
],
"one-line": [
false
],
"one-variable-per-declaration": [
false
],
"whitespace": [
true,
"check-postbrace"
]
},
"rulesDirectory": []
}

View File

@ -0,0 +1,19 @@
Password Reset Request
---
Hi,
if you have not requested a new password you can ignore this email. To set a new password for your account
just follow this link: {{ recover }}
See you soon
---
<h1>Hi, </h1>
<p>
if you have not requested a new password you can ignore this email. To set a new password for your account
just follow this link: <a href="{{ recover }}">{{ recover }}</a>
</p>
<p>
See you soon
</p>

View File

@ -0,0 +1,14 @@
Welcome to OhMyForm
---
Welcome to OhMyForm!
please confirm your account by following the following link: {{ confirm }}
enjoy!
---
<h1>Welcome to OhMyForm!</h1>
<p>
please confirm your account by following the following link: <a href="{{ confirm }}">{{ confirm }}</a>
</p>
<p>enjoy!</p>

View File

@ -5,7 +5,8 @@
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema,
crypto = require('crypto'),
crypto = require('crypto'),
bcrypt = require('bcrypt'),
config = require('../../config/config'),
timeStampPlugin = require('../libs/timestamp.server.plugin'),
path = require('path'),
@ -109,29 +110,37 @@ UserSchema.virtual('password').get(function () {
* Create instance method for hashing a password
*/
UserSchema.statics.hashPassword = UserSchema.methods.hashPassword = function(password) {
var encoding = 'base64';
var iterations = 10000;
var keylen = 128;
var size = 64;
var digest = 'SHA1';
//Generate salt if it doesn't exist yet
if(!this.salt){
this.salt = crypto.randomBytes(size).toString(encoding);
}
if (password) {
return crypto.pbkdf2Sync(password, new Buffer(this.salt, encoding), iterations, keylen, digest).toString(encoding);
} else {
return password;
}
return bcrypt.hashSync(password, 4);
};
/**
* Create instance method for authenticating user
*/
UserSchema.methods.authenticate = function(password) {
return this.password === this.hashPassword(password);
if (this.password[0] === '$') {
return bcrypt.compareSync(password, this.password);
}
var encoding = 'base64';
var iterations = 10000;
var keylen = 128;
var size = 64;
var digest = 'SHA1';
//Generate salt if it doesn't exist yet
if(!this.salt){
this.salt = crypto.randomBytes(size).toString(encoding);
}
var hash;
if (password) {
hash = crypto.pbkdf2Sync(password, new Buffer(this.salt, encoding), iterations, keylen, digest).toString(encoding);
} else {
hash = password;
}
return this.password === hash;
};
/**
@ -167,4 +176,4 @@ UserSchema.methods.isAdmin = function() {
mongoose.model('User', UserSchema);
module.exports = mongoose.model('User');
module.exports = mongoose.model('User');

265
package-lock.json generated
View File

@ -1141,8 +1141,16 @@
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"dev": true
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
},
"are-we-there-yet": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"requires": {
"delegates": "^1.0.0",
"readable-stream": "^2.0.6"
}
},
"argparse": {
"version": "1.0.10",
@ -1536,6 +1544,22 @@
}
}
},
"bcrypt": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-3.0.6.tgz",
"integrity": "sha512-taA5bCTfXe7FUjKroKky9EXpdhkVvhE5owfxfLYodbrAR1Ul3juLmIQmIQBK4L9a5BuUcE6cqmwT+Da20lF9tg==",
"requires": {
"nan": "2.13.2",
"node-pre-gyp": "0.12.0"
},
"dependencies": {
"nan": {
"version": "2.13.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
"integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw=="
}
}
},
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
@ -2333,8 +2357,7 @@
"chownr": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz",
"integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==",
"dev": true
"integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A=="
},
"chrome-trace-event": {
"version": "1.0.2",
@ -2577,8 +2600,7 @@
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"coffeescript": {
"version": "1.10.0",
@ -2816,6 +2838,11 @@
"date-now": "^0.1.4"
}
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"consolidate": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.14.5.tgz",
@ -3455,6 +3482,11 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@ -3481,6 +3513,11 @@
"integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
"dev": true
},
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
},
"di": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
@ -5501,6 +5538,14 @@
}
}
},
"fs-minipass": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz",
"integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==",
"requires": {
"minipass": "^2.2.1"
}
},
"fs-write-stream-atomic": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
@ -6115,6 +6160,21 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"requires": {
"aproba": "^1.0.3",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.0",
"object-assign": "^4.1.0",
"signal-exit": "^3.0.0",
"string-width": "^1.0.1",
"strip-ansi": "^3.0.1",
"wide-align": "^1.1.0"
}
},
"gaze": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
@ -8323,6 +8383,11 @@
"integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
"dev": true
},
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
},
"has-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
@ -8802,6 +8867,14 @@
"integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=",
"dev": true
},
"ignore-walk": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz",
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
"requires": {
"minimatch": "^3.0.4"
}
},
"ignorefs": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ignorefs/-/ignorefs-1.2.0.tgz",
@ -9152,7 +9225,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"dev": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -11658,6 +11730,35 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"minipass": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.5.0.tgz",
"integrity": "sha512-9FwMVYhn6ERvMR8XFdOavRz4QK/VJV8elU1x50vYexf9lslDcWe/f4HBRxCPd185ekRSjU6CfYyJCECa/CQy7Q==",
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
},
"dependencies": {
"safe-buffer": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
},
"yallist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A=="
}
}
},
"minizlib": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz",
"integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
"requires": {
"minipass": "^2.2.1"
}
},
"mississippi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
@ -12394,6 +12495,31 @@
}
}
},
"needle": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz",
"integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==",
"requires": {
"debug": "^3.2.6",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"negotiator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
@ -12646,6 +12772,63 @@
}
}
},
"node-pre-gyp": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz",
"integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==",
"requires": {
"detect-libc": "^1.0.2",
"mkdirp": "^0.5.1",
"needle": "^2.2.1",
"nopt": "^4.0.1",
"npm-packlist": "^1.1.6",
"npmlog": "^4.0.2",
"rc": "^1.2.7",
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4"
},
"dependencies": {
"deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
},
"nopt": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
"requires": {
"abbrev": "1",
"osenv": "^0.1.4"
}
},
"rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"requires": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
}
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"requires": {
"glob": "^7.1.3"
}
}
}
},
"node-uuid": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
@ -13746,6 +13929,20 @@
"resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-2.1.3.tgz",
"integrity": "sha512-AgSt+cP5XMooho1Ppn8NB3FFaVWefV+qZoZncYTUSch2GAEwlYLcIIbT5YVkMlFeNHnfwOvc4HDlbvrB5BRxXA=="
},
"npm-bundled": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz",
"integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g=="
},
"npm-packlist": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.4.tgz",
"integrity": "sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw==",
"requires": {
"ignore-walk": "^3.0.1",
"npm-bundled": "^1.0.1"
}
},
"npm-run-path": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
@ -13755,6 +13952,17 @@
"path-key": "^2.0.0"
}
},
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"requires": {
"are-we-there-yet": "~1.1.2",
"console-control-strings": "~1.1.0",
"gauge": "~2.7.3",
"set-blocking": "~2.0.0"
}
},
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
@ -14053,8 +14261,7 @@
"os-homedir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
},
"os-locale": {
"version": "3.1.0",
@ -14106,14 +14313,12 @@
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
},
"osenv": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz",
"integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=",
"dev": true,
"requires": {
"os-homedir": "^1.0.0",
"os-tmpdir": "^1.0.0"
@ -16189,6 +16394,11 @@
"sparse-bitfield": "^3.0.3"
}
},
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
},
"saxes": {
"version": "3.1.11",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz",
@ -16310,8 +16520,7 @@
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"dev": true
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"set-value": {
"version": "2.0.0",
@ -17068,7 +17277,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -17309,6 +17517,32 @@
"integrity": "sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=",
"dev": true
},
"tar": {
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz",
"integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==",
"requires": {
"chownr": "^1.1.1",
"fs-minipass": "^1.2.5",
"minipass": "^2.3.5",
"minizlib": "^1.2.1",
"mkdirp": "^0.5.0",
"safe-buffer": "^5.1.2",
"yallist": "^3.0.3"
},
"dependencies": {
"safe-buffer": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
},
"yallist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A=="
}
}
},
"taskgroup": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/taskgroup/-/taskgroup-4.3.1.tgz",
@ -18635,7 +18869,6 @@
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"dev": true,
"requires": {
"string-width": "^1.0.2 || 2"
}

View File

@ -22,6 +22,7 @@
},
"dependencies": {
"async": "^2.6.3",
"bcrypt": "^3.0.6",
"body-parser": "^1.19.0",
"bower": "^1.8.8",
"chalk": "^2.4.2",

13
ui/.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

23
ui/.eslintrc.js Normal file
View File

@ -0,0 +1,23 @@
module.exports = {
root: true,
env: {
browser: true,
node: true
},
parserOptions: {
parser: 'babel-eslint'
},
extends: [
'@nuxtjs',
'plugin:nuxt/recommended',
'plugin:prettier/recommended',
'prettier',
'prettier/vue'
],
plugins: [
'prettier'
],
// add your custom rules here
rules: {
}
}

84
ui/.gitignore vendored Normal file
View File

@ -0,0 +1,84 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE
.idea
# Service worker
sw.*

4
ui/.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

14
ui/Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM node:10 AS builder
MAINTAINER OhMyForm <admin@ohmyform.com>
WORKDIR /opt/app
# just copy everhing
COPY . .
RUN yarn install --frozen-lockfile
RUN yarn build
FROM nginx
COPY --from=builder /opt/app/dist /var/share/nginx/html

26
ui/README.md Normal file
View File

@ -0,0 +1,26 @@
# OhMyForm
> Opensource alternative to TypeForm
## Design
https://github.com/ohmyform/ohmyform/issues/13
## Build Setup
``` bash
# install dependencies
$ yarn install
# serve with hot reload at localhost:3000
$ yarn run dev
# build for production and launch server
$ yarn run build
$ yarn start
# generate static project
$ yarn run generate
```
For detailed explanation on how things work, checkout [Nuxt.js docs](https://nuxtjs.org).

56
ui/assets/css/base.scss Normal file
View File

@ -0,0 +1,56 @@
$accent: #fae596;
$primary: #3fb0ac;
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
1html {
font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 16px;
word-spacing: 1px;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
box-sizing: border-box;
}
.bg-primary {
&.navbar-dark,
&.dark {
background-color: #173e43 !important;
}
&.dark {
a {
color: #dddfd4;
&:hover {
color: #fae596;
}
}
}
}
.btn-primary {
color: #fff;
background-color: $primary;
border-color: $primary;
&:hover {
color: #fff;
background-color: darken($primary, 10%);
border-color: darken($primary, 10%);
}
&:not(:disabled):not(.disabled):active {
color: #fff;
background-color: darken($primary, 10%);
border-color: darken($primary, 10%);
}
&:not(:disabled):not(.disabled):active:focus,
&:focus {
box-shadow: 0 0 0 0.2rem transparentize($primary, 0.5);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

57
ui/layouts/admin.vue Normal file
View File

@ -0,0 +1,57 @@
<template>
<div>
<b-navbar variant="primary" toggleable="sm" type="dark">
<b-navbar-brand href="https://ohmyform.com">
<img src="../assets/img/logo_white_small.png" style="height: 27px" />
</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav>
<b-nav-item
:active="/\/admin\/forms/.test($route.fullPath)"
to="/admin/forms"
>
Forms
</b-nav-item>
<b-nav-item
:active="/\/admin\/users/.test($route.fullPath)"
to="/admin/users"
>
Users
</b-nav-item>
<b-nav-item
:active="/\/admin\/configuration/.test($route.fullPath)"
to="/admin/configuration"
>
Configuration
</b-nav-item>
</b-navbar-nav>
<b-navbar-nav class="ml-auto">
<b-nav-item-dropdown right>
<template slot="button-content">
<font-awesome-icon icon="user-circle" />
</template>
<b-dropdown-item to="/admin/me">Profile</b-dropdown-item>
<b-dropdown-item @click="logout">Sign Out</b-dropdown-item>
</b-nav-item-dropdown>
</b-navbar-nav>
</b-collapse>
</b-navbar>
<nuxt />
</div>
</template>
<script>
export default {
methods: {
async logout() {
await this.$auth.logout()
this.$router.push('/login')
}
}
}
</script>

5
ui/layouts/default.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<div>
<nuxt />
</div>
</template>

40
ui/layouts/screen.vue Normal file
View File

@ -0,0 +1,40 @@
<template>
<div class="screen bg-primary dark">
<div class="content">
<nuxt />
</div>
<div class="footer">
<nuxt-link to="/login">Login</nuxt-link>
<nuxt-link to="/register">Register</nuxt-link>
<nuxt-link to="/admin">Manage</nuxt-link>
<a href="https://ohmyform.com">OhMyForm</a>
</div>
</div>
</template>
<style lang="scss" scoped>
.screen {
min-height: 100vh;
display: flex;
flex-direction: column;
.content {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.footer {
height: 40px;
text-align: center;
a {
padding-right: 16px;
padding-left: 16px;
}
}
}
</style>

106
ui/nuxt.config.js Normal file
View File

@ -0,0 +1,106 @@
import pkg from './package'
export default {
mode: 'spa',
/*
** Headers of the page
*/
head: {
title: pkg.name,
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: pkg.description }
],
link: [{ rel: 'icon', type: 'image/png', href: '/favicon.png' }]
},
proxy: {
'/api': { target: 'http://localhost:3000', pathRewrite: { '/api/': '/' } }
},
router: {
middleware: ['auth']
},
server: {
port: 3100
},
auth: {
strategies: {
local: {
endpoints: {
login: { url: '/api/auth/login', method: 'post', propertyName: 'accessToken' },
logout: { url: '/api/auth/logout', method: 'post' },
user: false
},
tokenRequired: true,
tokenType: 'Bearer'
}
},
redirect: {
login: '/login',
logout: '/',
home: '/admin'
}
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [
'@/assets/css/base.scss'
],
/*
** Plugins to load before mounting the App
*/
plugins: [
'@/plugins/font-awesome.js',
'@/plugins/fab.js'
],
/*
** Nuxt.js modules
*/
modules: [
'@nuxtjs/auth',
'@nuxtjs/proxy',
'@nuxtjs/axios',
'bootstrap-vue/nuxt'
],
/*
** Axios module configuration
*/
axios: {
// See https://github.com/nuxt-community/axios-module#options
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend(config, ctx) {
// Run ESLint on save
if (ctx.isDev && ctx.isClient) {
config.module.rules.push({
enforce: 'pre',
test: /\.(js|vue)$/,
loader: 'eslint-loader',
exclude: /(node_modules)/
})
}
}
}
}

48
ui/package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "OhMyForm",
"version": "1.0.0",
"description": "Opensource alternative to TypeForm",
"author": "Michael Schramm",
"private": true,
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
"precommit": "npm run lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.22",
"@fortawesome/free-solid-svg-icons": "^5.10.2",
"@fortawesome/vue-fontawesome": "^0.1.7",
"@nuxtjs/auth": "^4.8.1",
"@nuxtjs/axios": "^5.3.6",
"@nuxtjs/proxy": "^1.3.3",
"bootstrap": "^4.1.3",
"bootstrap-vue": "^2.0.0-rc.11",
"cross-env": "^5.2.0",
"node-sass": "^4.12.0",
"nuxt": "^2.4.0",
"sass-loader": "^8.0.0",
"vue-fab": "^2.3.1"
},
"devDependencies": {
"@nuxtjs/eslint-config": "^0.0.1",
"babel-eslint": "^10.0.1",
"eslint": "^5.15.1",
"eslint-config-prettier": "^4.1.0",
"eslint-config-standard": ">=12.0.0",
"eslint-loader": "^2.1.2",
"eslint-plugin-import": ">=2.16.0",
"eslint-plugin-jest": ">=22.3.0",
"eslint-plugin-node": ">=8.0.1",
"eslint-plugin-nuxt": ">=0.4.2",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-promise": ">=4.0.1",
"eslint-plugin-standard": ">=4.0.0",
"eslint-plugin-vue": "^5.2.2",
"nodemon": "^1.18.9",
"prettier": "^1.16.4"
}
}

View File

@ -0,0 +1,11 @@
<template>
<div>
config XD
</div>
</template>
<script>
export default {
layout: 'admin'
}
</script>

View File

@ -0,0 +1,22 @@
<template>
<div>Form: {{ form }}</div>
</template>
<script>
export default {
layout: 'admin',
data() {
return {
form: null
}
},
created() {
this.load()
},
methods: {
async load() {
this.form = await this.$axios.$get(`/api/forms/${this.$route.params.id}`)
}
}
}
</script>

View File

@ -0,0 +1,46 @@
<template>
<div>
<b-alert show variant="info" class="m-3">
All created forms, they are publicly visible if live is true
</b-alert>
<fab bg-color="#173e43" />
<b-table striped hover :items="provider" :fields="fields">
<template slot="[menu]" slot-scope="data">
<nuxt-link :to="'/admin/forms/' + data.item.id">Open</nuxt-link>
</template>
</b-table>
</div>
</template>
<script>
export default {
layout: 'admin',
data() {
return {
fields: [
{
key: 'title'
},
{
key: 'created'
},
{
key: 'live'
},
{
key: 'responses'
},
{
key: 'menu'
}
]
}
},
methods: {
provider(ctx) {
return this.$axios.$get('/api/forms')
}
}
}
</script>

11
ui/pages/admin/index.vue Normal file
View File

@ -0,0 +1,11 @@
<template>
<div>
ADMIN
</div>
</template>
<script>
export default {
layout: 'admin'
}
</script>

11
ui/pages/admin/me.vue Normal file
View File

@ -0,0 +1,11 @@
<template>
<div>
My Profile XD
</div>
</template>
<script>
export default {
layout: 'admin'
}
</script>

View File

@ -0,0 +1,22 @@
<template>
<div>User: {{ user }}</div>
</template>
<script>
export default {
layout: 'admin',
data() {
return {
user: null
}
},
created() {
this.load()
},
methods: {
async load() {
this.user = await this.$axios.$get(`/api/users/${this.$route.params.id}`)
}
}
}
</script>

Some files were not shown because too many files have changed in this diff Show More