├── client ├── src │ ├── components │ │ ├── ui │ │ │ ├── users │ │ │ │ ├── UsersView.scss │ │ │ │ ├── UserDetailView.vue │ │ │ │ └── UsersView.vue │ │ │ ├── auth │ │ │ │ ├── SigninView.scss │ │ │ │ ├── LoginView.vue │ │ │ │ └── SigninView.vue │ │ │ ├── divisions │ │ │ │ ├── DivisionDetailView.vue │ │ │ │ └── DivisionsView.vue │ │ │ └── books │ │ │ │ ├── BookDetailView.vue │ │ │ │ └── BooksView.vue │ │ ├── layout │ │ │ ├── HeaderMenu.scss │ │ │ ├── FooterMenu.scss │ │ │ ├── FooterMenu.vue │ │ │ └── HeaderMenu.vue │ │ └── HomeView.vue │ ├── utils │ │ ├── index.ts │ │ ├── Authorize.ts │ │ └── Validators.ts │ ├── robots.txt │ ├── shims-vue.d.ts │ ├── views │ │ ├── About.vue │ │ └── Home.vue │ ├── assets │ │ ├── images │ │ │ ├── logo │ │ │ │ ├── logo.png │ │ │ │ ├── logo2.png │ │ │ │ └── favicon.ico │ │ │ └── github │ │ │ │ ├── logo │ │ │ │ ├── GitHub_Logo.png │ │ │ │ └── GitHub_Logo_White.png │ │ │ │ ├── octocat │ │ │ │ ├── Octocat.jpg │ │ │ │ └── Octocat.png │ │ │ │ └── mark │ │ │ │ └── PNG │ │ │ │ ├── GitHub-Mark-32px.png │ │ │ │ ├── GitHub-Mark-64px.png │ │ │ │ ├── GitHub-Mark-120px-plus.png │ │ │ │ ├── GitHub-Mark-Light-32px.png │ │ │ │ ├── GitHub-Mark-Light-64px.png │ │ │ │ └── GitHub-Mark-Light-120px-plus.png │ │ ├── scss │ │ │ ├── custom.scss │ │ │ ├── variable.scss │ │ │ └── global.scss │ │ └── languages │ │ │ ├── messages.ts │ │ │ └── nation │ │ │ ├── ja.ts │ │ │ ├── ch.ts │ │ │ ├── ko.ts │ │ │ └── en.ts │ ├── types │ │ └── index.ts │ ├── shims-tsx.d.ts │ ├── store │ │ ├── modules │ │ │ ├── stocks.js │ │ │ └── portfolio.js │ │ └── index.ts │ ├── registerServiceWorker.ts │ ├── main.ts │ ├── App.vue │ ├── router │ │ └── index.ts │ └── models │ │ └── index.ts ├── .browserslistrc ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── img │ │ └── icons │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── mstile-150x150.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon-60x60.png │ │ │ ├── apple-touch-icon-76x76.png │ │ │ ├── apple-touch-icon-120x120.png │ │ │ ├── apple-touch-icon-152x152.png │ │ │ ├── apple-touch-icon-180x180.png │ │ │ ├── msapplication-icon-144x144.png │ │ │ └── safari-pinned-tab.svg │ ├── manifest.json │ └── index.html ├── babel.config.js ├── tests │ └── unit │ │ ├── .eslintrc.js │ │ └── example.spec.ts ├── postcss.config.js ├── .gitignore ├── .eslintrc.js ├── README.md ├── tsconfig.json ├── jest.config.js └── package.json ├── server ├── db │ ├── Dockerfile │ └── default.sql ├── tsconfig.test.json ├── .env ├── src │ ├── app │ │ ├── routes │ │ │ ├── auth │ │ │ │ ├── index.ts │ │ │ │ ├── UserRouter.ts │ │ │ │ └── AuthenticationRouter.ts │ │ │ └── graphql │ │ │ │ ├── query │ │ │ │ ├── index.ts │ │ │ │ ├── BookQuery.ts │ │ │ │ ├── AuthorityQuery.ts │ │ │ │ ├── DivisionQuery.ts │ │ │ │ └── UserQuery.ts │ │ │ │ ├── mutation │ │ │ │ ├── index.ts │ │ │ │ ├── DivisionMutation.ts │ │ │ │ ├── AuthorityMutation.ts │ │ │ │ ├── BookMutation.ts │ │ │ │ └── UserMutation.ts │ │ │ │ ├── schema │ │ │ │ └── index.ts │ │ │ │ └── type │ │ │ │ └── index.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── model │ │ │ │ ├── index.ts │ │ │ │ ├── AuthorityModel.ts │ │ │ │ ├── BookModel.ts │ │ │ │ ├── DivisionModel.ts │ │ │ │ ├── UserAuthorityModel.ts │ │ │ │ └── UserModel.ts │ │ │ ├── repository │ │ │ │ ├── index.ts │ │ │ │ ├── AbstractRepository.ts │ │ │ │ ├── UserRepository.ts │ │ │ │ ├── BookRepository.ts │ │ │ │ ├── DivisionRepository.ts │ │ │ │ └── AuthorityRepository.ts │ │ │ ├── BookService.ts │ │ │ ├── AuthorityService.ts │ │ │ ├── DivisionService.ts │ │ │ └── UserService.ts │ │ ├── utils │ │ │ └── PasswordEncoderUtils.ts │ │ └── types │ │ │ └── index.ts │ ├── config │ │ ├── environments │ │ │ ├── test.ts │ │ │ ├── production.ts │ │ │ ├── develop.ts │ │ │ └── index.ts │ │ ├── logger │ │ │ └── index.ts │ │ ├── database │ │ │ ├── index.ts │ │ │ └── default │ │ │ │ └── index.ts │ │ └── auth │ │ │ └── passport.ts │ ├── test │ │ └── repository │ │ │ ├── book │ │ │ └── BookRepository.test.ts │ │ │ ├── division │ │ │ └── DivisionRepository.test.ts │ │ │ └── user │ │ │ └── UserRepository.test.ts │ └── app.ts ├── .editorconfig ├── Dockerfile ├── tsconfig.json ├── docker-compose.yml ├── .babelrc ├── README.md ├── .gitignore ├── tslint.json └── package.json ├── docker-elk ├── extensions │ ├── README.md │ └── logspout │ │ ├── Dockerfile │ │ ├── modules.go │ │ ├── logspout-compose.yml │ │ ├── build.sh │ │ └── README.md ├── kibana │ ├── Dockerfile │ └── config │ │ └── kibana.yml ├── logstash │ ├── Dockerfile │ ├── pipeline │ │ └── logstash.conf │ └── config │ │ └── logstash.yml ├── elasticsearch │ ├── Dockerfile │ └── config │ │ └── elasticsearch.yml ├── .travis.yml ├── docker-compose.yml └── README.md ├── greenkeeper.json ├── .editorconfig ├── client-node-port.yaml ├── client-pod.yaml ├── .gitignore └── README.md /client/src/components/ui/users/UsersView.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /client/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Validators"; 2 | -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/app"] 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org/ 2 | 3 | User-agent: * 4 | Disallow: 5 | -------------------------------------------------------------------------------- /client/src/components/ui/auth/SigninView.scss: -------------------------------------------------------------------------------- 1 | .error-msg { 2 | color: orangered; 3 | } 4 | -------------------------------------------------------------------------------- /client/tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /server/db/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mariadb/server:10.3 2 | 3 | ADD default.sql /docker-entrypoint-initdb.d 4 | -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /docker-elk/extensions/README.md: -------------------------------------------------------------------------------- 1 | Third-party extensions that enable extra integrations with the ELK stack. 2 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue"; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /server/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /client/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /client/src/assets/images/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/src/assets/images/logo/logo.png -------------------------------------------------------------------------------- /client/src/assets/images/logo/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/src/assets/images/logo/logo2.png -------------------------------------------------------------------------------- /client/public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /client/public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /client/public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /client/src/assets/images/logo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/src/assets/images/logo/favicon.ico -------------------------------------------------------------------------------- /client/public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /greenkeeper.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": { 3 | "default": { 4 | "packages": ["client/package.json", "server/package.json"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface ApolloResponse { 2 | data: any; 3 | loading: boolean; 4 | networkStatus: number; 5 | stale: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /server/db/default.sql: -------------------------------------------------------------------------------- 1 | create user 'hunseol'@'localhost' identified by 'hunseol'; 2 | grant all privileges on shooney_management.* to 'hunseol'@'localhost'; 3 | -------------------------------------------------------------------------------- /client/public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /client/public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /client/public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /client/public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /client/public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /client/public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /client/public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /client/src/assets/images/github/logo/GitHub_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/src/assets/images/github/logo/GitHub_Logo.png -------------------------------------------------------------------------------- /client/src/assets/images/github/octocat/Octocat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/src/assets/images/github/octocat/Octocat.jpg -------------------------------------------------------------------------------- /client/src/assets/images/github/octocat/Octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/src/assets/images/github/octocat/Octocat.png -------------------------------------------------------------------------------- /client/public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /server/.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | SERVER_PORT=4000 3 | DB_PORT=3306 4 | DB_HOST=mariadb 5 | DB_USER=hunseol 6 | DB_ROOT_PASSWORD=hunseol 7 | DB_DATABASE=shooney_management 8 | -------------------------------------------------------------------------------- /client/src/assets/images/github/logo/GitHub_Logo_White.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/src/assets/images/github/logo/GitHub_Logo_White.png -------------------------------------------------------------------------------- /server/src/app/routes/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { auth_router } from './AuthenticationRouter'; 2 | import { user_router } from './UserRouter'; 3 | 4 | export { auth_router, user_router }; 5 | -------------------------------------------------------------------------------- /client/src/assets/images/github/mark/PNG/GitHub-Mark-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/src/assets/images/github/mark/PNG/GitHub-Mark-32px.png -------------------------------------------------------------------------------- /client/src/assets/images/github/mark/PNG/GitHub-Mark-64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/src/assets/images/github/mark/PNG/GitHub-Mark-64px.png -------------------------------------------------------------------------------- /client/src/assets/images/github/mark/PNG/GitHub-Mark-120px-plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/src/assets/images/github/mark/PNG/GitHub-Mark-120px-plus.png -------------------------------------------------------------------------------- /client/src/assets/images/github/mark/PNG/GitHub-Mark-Light-32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/src/assets/images/github/mark/PNG/GitHub-Mark-Light-32px.png -------------------------------------------------------------------------------- /client/src/assets/images/github/mark/PNG/GitHub-Mark-Light-64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/src/assets/images/github/mark/PNG/GitHub-Mark-Light-64px.png -------------------------------------------------------------------------------- /client/src/assets/images/github/mark/PNG/GitHub-Mark-Light-120px-plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seolhun/vue-type-graphql-example/HEAD/client/src/assets/images/github/mark/PNG/GitHub-Mark-Light-120px-plus.png -------------------------------------------------------------------------------- /docker-elk/extensions/logspout/Dockerfile: -------------------------------------------------------------------------------- 1 | # uses ONBUILD instructions described here: 2 | # https://github.com/gliderlabs/logspout/tree/master/custom 3 | 4 | FROM gliderlabs/logspout:master 5 | ENV SYSLOG_FORMAT rfc3164 6 | -------------------------------------------------------------------------------- /docker-elk/kibana/Dockerfile: -------------------------------------------------------------------------------- 1 | # https://github.com/elastic/kibana-docker 2 | FROM docker.elastic.co/kibana/kibana-oss:6.2.2 3 | 4 | # Add your kibana plugins setup here 5 | # Example: RUN kibana-plugin install 6 | -------------------------------------------------------------------------------- /docker-elk/logstash/Dockerfile: -------------------------------------------------------------------------------- 1 | # https://github.com/elastic/logstash-docker 2 | FROM docker.elastic.co/logstash/logstash-oss:6.2.2 3 | 4 | # Add your logstash plugins setup here 5 | # Example: RUN logstash-plugin install logstash-filter-json 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /docker-elk/elasticsearch/Dockerfile: -------------------------------------------------------------------------------- 1 | # https://github.com/elastic/elasticsearch-docker 2 | FROM docker.elastic.co/elasticsearch/elasticsearch-oss:6.2.2 3 | 4 | # Add your elasticsearch plugins setup here 5 | # Example: RUN elasticsearch-plugin install analysis-icu 6 | -------------------------------------------------------------------------------- /docker-elk/logstash/pipeline/logstash.conf: -------------------------------------------------------------------------------- 1 | input { 2 | tcp { 3 | port => 5000 4 | } 5 | } 6 | 7 | ## Add your filters / logstash plugins configuration here 8 | 9 | output { 10 | elasticsearch { 11 | hosts => "elasticsearch:9200" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client-node-port.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: client-node-port 5 | spec: 6 | type: NodePort 7 | ports: 8 | - port: 3050 9 | targetPort: 3000 10 | nodePort: 31515 11 | selector: 12 | component: web 13 | -------------------------------------------------------------------------------- /server/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /client-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: client-pod 5 | labels: 6 | component: web 7 | spec: 8 | containers: 9 | - name: client 10 | image: seolhun/multi-client 11 | ports: 12 | - containerPort: 3000 13 | -------------------------------------------------------------------------------- /docker-elk/logstash/config/logstash.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ## Default Logstash configuration from logstash-docker. 3 | ## from https://github.com/elastic/logstash-docker/blob/master/build/logstash/config/logstash-oss.yml 4 | # 5 | http.host: "0.0.0.0" 6 | path.config: /usr/share/logstash/pipeline 7 | -------------------------------------------------------------------------------- /docker-elk/kibana/config/kibana.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ## Default Kibana configuration from kibana-docker. 3 | ## from https://github.com/elastic/kibana-docker/blob/master/build/kibana/config/kibana.yml 4 | # 5 | server.name: kibana 6 | server.host: "0" 7 | elasticsearch.url: http://elasticsearch:9200 8 | -------------------------------------------------------------------------------- /client/src/assets/scss/custom.scss: -------------------------------------------------------------------------------- 1 | /* ------------------------ 2 | Bootstrap Table 3 | ------------------------ */ 4 | 5 | .table-hover tr:hover { 6 | cursor: pointer; 7 | } 8 | 9 | .form-group > label { 10 | font-weight: bold; 11 | } 12 | 13 | .jumbotron { 14 | padding: 20px; 15 | } 16 | -------------------------------------------------------------------------------- /server/src/app/routes/auth/UserRouter.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import { passport } from '../../../config/auth/passport'; 4 | 5 | const user_router = Router(); 6 | 7 | user_router.get('/current', (req, res) => { 8 | res.send(req.user); 9 | }); 10 | 11 | export { user_router }; 12 | -------------------------------------------------------------------------------- /server/src/app/services/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthorityService } from "./AuthorityService"; 2 | import { BookService } from "./BookService"; 3 | import { DivisionService } from "./DivisionService"; 4 | import { UserService } from "./UserService"; 5 | 6 | export { AuthorityService, BookService, DivisionService, UserService }; 7 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /client/src/assets/languages/messages.ts: -------------------------------------------------------------------------------- 1 | import VueI18n from "vue-i18n"; 2 | 3 | import ch from "./nation/ch"; 4 | import en from "./nation/en"; 5 | import ja from "./nation/ja"; 6 | import ko from "./nation/ko"; 7 | 8 | const messages = { 9 | ko, 10 | en, 11 | ja, 12 | ch 13 | }; 14 | 15 | export default messages; 16 | -------------------------------------------------------------------------------- /docker-elk/extensions/logspout/modules.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // installs the Logstash adapter for Logspout, and required dependencies 4 | // https://github.com/looplab/logspout-logstash 5 | import ( 6 | _ "github.com/looplab/logspout-logstash" 7 | _ "github.com/gliderlabs/logspout/transports/udp" 8 | _ "github.com/gliderlabs/logspout/transports/tcp" 9 | ) 10 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | # Builder 2 | FROM mhart/alpine-node:10 AS builder 3 | WORKDIR /app 4 | COPY . . 5 | RUN yarn install 6 | RUN yarn build 7 | 8 | # Runner 9 | FROM mhart/alpine-node:slim-10 AS runner 10 | WORKDIR /app 11 | COPY --from=builder ./app/dist . 12 | COPY --from=builder ./app/node_modules ./node_modules 13 | 14 | EXPOSE 4000 15 | CMD ["node", "app.js"] 16 | -------------------------------------------------------------------------------- /server/src/app/services/model/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthorityModel } from './AuthorityModel'; 2 | import { BookModel } from './BookModel'; 3 | import { DivisionModel } from './DivisionModel'; 4 | import { UserAuthorityModel } from './UserAuthorityModel'; 5 | import { UserModel } from './UserModel'; 6 | 7 | export { AuthorityModel, BookModel, DivisionModel, UserAuthorityModel, UserModel }; 8 | -------------------------------------------------------------------------------- /client/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from "vue"; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/components/layout/HeaderMenu.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/scss/variable"; 2 | 3 | /* ------------------------ 4 | Header CSS 5 | ------------------------ */ 6 | .navbar-brand { 7 | font-weight: 800; 8 | } 9 | 10 | .nav-item > li { 11 | color: $active-color !important; 12 | } 13 | 14 | .dropdown-item { 15 | & .active { 16 | color: $ocean !important; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/src/app/services/repository/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthorityRepository } from "./AuthorityRepository"; 2 | import { BookRepository } from "./BookRepository"; 3 | import { DivisionRepository } from "./DivisionRepository"; 4 | import { UserRepository } from "./UserRepository"; 5 | 6 | export { 7 | AuthorityRepository, 8 | BookRepository, 9 | DivisionRepository, 10 | UserRepository 11 | }; 12 | -------------------------------------------------------------------------------- /client/tests/unit/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from "@vue/test-utils"; 2 | import HelloWorld from "@/components/HelloWorld.vue"; 3 | 4 | describe("HelloWorld.vue", () => { 5 | it("renders props.msg when passed", () => { 6 | const msg = "new message"; 7 | const wrapper = shallowMount(HelloWorld, { 8 | propsData: { msg } 9 | }); 10 | expect(wrapper.text()).toMatch(msg); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /client/src/components/layout/FooterMenu.scss: -------------------------------------------------------------------------------- 1 | @import "../../assets/scss/variable"; 2 | 3 | .footer { 4 | -ms-flex: none; 5 | flex: none; 6 | background-color: $dark-black; 7 | } 8 | 9 | .footer-div { 10 | color: $bg-color; 11 | } 12 | 13 | .footer-list-div > p { 14 | list-style-type: none; 15 | margin: 3px; 16 | } 17 | 18 | .footer-list-div > p > a { 19 | color: $active-color; 20 | word-break: break-all; 21 | } 22 | -------------------------------------------------------------------------------- /docker-elk/extensions/logspout/logspout-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | logspout: 5 | build: 6 | context: extensions/logspout 7 | volumes: 8 | - /var/run/docker.sock:/var/run/docker.sock:ro 9 | environment: 10 | ROUTE_URIS: logstash://logstash:5000 11 | LOGSTASH_TAGS: docker-elk 12 | networks: 13 | - elk 14 | depends_on: 15 | - logstash 16 | restart: on-failure 17 | -------------------------------------------------------------------------------- /client/src/utils/Authorize.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @export 3 | * @returns { boolean } 4 | */ 5 | export function isAuthorized(): boolean { 6 | if (!window.localStorage.getItem("TOKEN")) { 7 | return false; 8 | } 9 | const lifeTime = 10 | JSON.parse(window.localStorage.getItem("TOKEN") || "").lifeTime * 1000; 11 | const nowTime = new Date().getTime(); 12 | if (nowTime > lifeTime) { 13 | return false; 14 | } 15 | return true; 16 | } 17 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "strictNullChecks": true, 10 | "allowSyntheticDefaultImports": true, 11 | "baseUrl": "src", 12 | "outDir": "dist" 13 | }, 14 | "exclude": ["node_modules", "src/test"] 15 | } 16 | -------------------------------------------------------------------------------- /server/src/app/utils/PasswordEncoderUtils.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcryptjs"; 2 | 3 | class PasswordEncoderUtils { 4 | static bcryptedPasswordSync(password: string): string { 5 | const salt = bcrypt.genSaltSync(10); 6 | return bcrypt.hashSync(password, salt); 7 | } 8 | 9 | static compareBcryptedPasswordSync( 10 | password: string, 11 | db_password: string 12 | ): boolean { 13 | return bcrypt.compareSync(password, db_password); 14 | } 15 | } 16 | 17 | export { PasswordEncoderUtils }; 18 | -------------------------------------------------------------------------------- /server/src/app/routes/graphql/query/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from "graphql"; 2 | 3 | import { AuthorityQuery } from "./AuthorityQuery"; 4 | import { BookQuery } from "./BookQuery"; 5 | import { DivisionQuery } from "./DivisionQuery"; 6 | import { UserQuery } from "./UserQuery"; 7 | 8 | const query = new GraphQLObjectType({ 9 | name: "RootQuery", 10 | fields: { 11 | ...AuthorityQuery, 12 | ...BookQuery, 13 | ...DivisionQuery, 14 | ...UserQuery 15 | } 16 | }); 17 | 18 | export { query }; 19 | -------------------------------------------------------------------------------- /client/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /server/src/app/routes/graphql/mutation/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLObjectType } from "graphql"; 2 | 3 | import { AuthorityMutation } from "./AuthorityMutation"; 4 | import { BookMutation } from "./BookMutation"; 5 | import { DivisionMutation } from "./DivisionMutation"; 6 | import { UserMutation } from "./UserMutation"; 7 | 8 | const mutation = new GraphQLObjectType({ 9 | name: "Mutation", 10 | fields: { 11 | ...AuthorityMutation, 12 | ...BookMutation, 13 | ...DivisionMutation, 14 | ...UserMutation 15 | } 16 | }); 17 | 18 | export { mutation }; 19 | -------------------------------------------------------------------------------- /server/src/app/routes/graphql/schema/index.ts: -------------------------------------------------------------------------------- 1 | import { graphql, GraphQLSchema, printSchema } from "graphql"; 2 | import { mutation } from "../mutation"; 3 | import { query } from "../query"; 4 | 5 | export function getSchemaString() { 6 | return printSchema(schema); 7 | } 8 | 9 | export async function execute( 10 | request: string, 11 | variables?: { [key: string]: any } 12 | ) { 13 | return graphql(schema, request, null, {}, variables); 14 | } 15 | 16 | const schema = new GraphQLSchema({ 17 | query, 18 | mutation 19 | }); 20 | 21 | export { schema }; 22 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-type-graphql-example", 3 | "short_name": "vue-type-graphql-example", 4 | "icons": [ 5 | { 6 | "src": "./img/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./img/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "./index.html", 17 | "display": "standalone", 18 | "background_color": "#000000", 19 | "theme_color": "#4DBA87" 20 | } 21 | -------------------------------------------------------------------------------- /server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | mariadb: 4 | build: ./db 5 | ports: 6 | - ${DB_PORT} 7 | environment: 8 | - MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD} 9 | - MARIADB_USER=${DB_USER} 10 | - MARIADB_PASSWORD=${DB_ROOT_PASSWORD} 11 | - MARIADB_DATABASE=${DB_DATABASE} 12 | env_file: 13 | - .env 14 | 15 | server: 16 | build: ./ 17 | env_file: 18 | - .env 19 | ports: 20 | - ${SERVER_PORT}:${SERVER_PORT} 21 | depends_on: 22 | - ${DB_HOST} 23 | links: 24 | - mariadb 25 | -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ["plugin:vue/essential", "@vue/prettier", "@vue/typescript"], 7 | rules: { 8 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 9 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" 10 | }, 11 | parserOptions: { 12 | parser: "@typescript-eslint/parser" 13 | }, 14 | overrides: [ 15 | { 16 | files: ["**/__tests__/*.{j,t}s?(x)"], 17 | env: { 18 | jest: true 19 | } 20 | } 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /server/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "browsers": [ 9 | "> 1%", 10 | "last 2 versions", 11 | "not ie <= 8" 12 | ] 13 | } 14 | } 15 | ], 16 | "stage-2" 17 | ], 18 | "plugins": [ 19 | "transform-runtime" 20 | ], 21 | "env": { 22 | "test": { 23 | "presets": [ 24 | "env", 25 | "stage-2" 26 | ], 27 | "plugins": [ 28 | "istanbul" 29 | ] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/src/config/environments/test.ts: -------------------------------------------------------------------------------- 1 | import { SettingEnvironments } from './'; 2 | 3 | export default { 4 | NODE_ENV: 'test', 5 | EXPRESS_PORT: 4000, 6 | 7 | // Session Key 8 | COOKIE_SESSION_MAX_AGE: 1000 * 60 * 60 * 24 * 30, 9 | COOKIE_SESSION_KEYS: ['seolhun-hicord-examples'], 10 | SESSION_MAX_AGE: 1000 * 60 * 30, 11 | SESSION_KEYS: ['seolhun-hicord-examples'], 12 | 13 | // Oauth2 14 | FACEBOOK_CLIENT_ID: '', 15 | FACEBOOK_CLIENT_SECRET_ID: '', 16 | GITHUB_CLIENT_ID: '', 17 | GITHUB_CLIENT_SECRET_ID: '', 18 | GOOGLE_CLIENT_ID: '', 19 | GOOGLE_CLIENT_SECRET_ID: '', 20 | } as SettingEnvironments; 21 | -------------------------------------------------------------------------------- /server/src/config/environments/production.ts: -------------------------------------------------------------------------------- 1 | import { SettingEnvironments } from './'; 2 | 3 | export default { 4 | NODE_ENV: 'production', 5 | EXPRESS_PORT: 4000, 6 | 7 | // Session Key 8 | COOKIE_SESSION_MAX_AGE: 1000 * 60 * 60 * 24 * 30, 9 | COOKIE_SESSION_KEYS: ['seolhun-hicord-examples'], 10 | SESSION_MAX_AGE: 1000 * 60 * 30, 11 | SESSION_KEYS: ['seolhun-hicord-examples'], 12 | 13 | // Oauth2 14 | FACEBOOK_CLIENT_ID: '', 15 | FACEBOOK_CLIENT_SECRET_ID: '', 16 | GITHUB_CLIENT_ID: '', 17 | GITHUB_CLIENT_SECRET_ID: '', 18 | GOOGLE_CLIENT_ID: '', 19 | GOOGLE_CLIENT_SECRET_ID: '', 20 | } as SettingEnvironments; 21 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Book Management System. 2 | 3 | - Author : [Seolhun](https://github.com/Seolhun) 4 | - Date : 2017.10.19 5 | 6 | ## Used Stacks 7 | 8 | 1. `TypeScript` 9 | 2. `NodeJS`, `Express` 10 | 3. `Vue` 11 | 4. `GraphQL` 12 | 5. `Apollo Client` 13 | 6. `Sequelize` 14 | 15 | ## How to run 16 | 17 | - `Client` 18 | 1. `yarn run dev` 19 | 2. [http://localhost:8080/](http://localhost:8080/) 20 | 21 | ## Demo 22 | 23 | [vue-type-graphql-example.surge.sh](vue-type-graphql-example.surge.sh) 24 | 25 | ## Reference 26 | 27 | - `Client` 28 | - [Vue](https://vuejs.org/) 29 | - [Vue Apollo Client](https://github.com/akryum/vue-apollo) 30 | -------------------------------------------------------------------------------- /docker-elk/extensions/logspout/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # unmodified from: 4 | # https://github.com/gliderlabs/logspout/blob/67ee3831cbd0594361bb3381380c65bdbeb3c20f/custom/build.sh 5 | 6 | set -e 7 | apk add --update go git mercurial build-base 8 | mkdir -p /go/src/github.com/gliderlabs 9 | cp -r /src /go/src/github.com/gliderlabs/logspout 10 | cd /go/src/github.com/gliderlabs/logspout 11 | export GOPATH=/go 12 | go get 13 | go build -ldflags "-X main.Version=$1" -o /bin/logspout 14 | apk del go git mercurial build-base 15 | rm -rf /go /var/cache/apk/* /root/.glide 16 | 17 | # backwards compatibility 18 | ln -fs /tmp/docker.sock /var/run/docker.sock 19 | -------------------------------------------------------------------------------- /server/src/config/environments/develop.ts: -------------------------------------------------------------------------------- 1 | import { SettingEnvironments } from './'; 2 | 3 | export default { 4 | NODE_ENV: 'development', 5 | EXPRESS_PORT: 4000, 6 | 7 | // Session Key 8 | COOKIE_SESSION_MAX_AGE: 1000 * 60 * 60 * 24 * 30, 9 | COOKIE_SESSION_KEYS: ['seolhun-hicord-examples'], 10 | SESSION_MAX_AGE: 1000 * 60 * 30, 11 | SESSION_KEYS: ['seolhun-hicord-examples'], 12 | 13 | // Oauth2 14 | FACEBOOK_CLIENT_ID: '', 15 | FACEBOOK_CLIENT_SECRET_ID: '', 16 | GITHUB_CLIENT_ID: '05096350eaddf80dbd34', 17 | GITHUB_CLIENT_SECRET_ID: '773371ce78f5f2578873f75f202759c0b28b85e0', 18 | GOOGLE_CLIENT_ID: '', 19 | GOOGLE_CLIENT_SECRET_ID: '', 20 | } as SettingEnvironments; 21 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-type-graphql-example 9 | 10 | 11 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docker-elk/elasticsearch/config/elasticsearch.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ## Default Elasticsearch configuration from elasticsearch-docker. 3 | ## from https://github.com/elastic/elasticsearch-docker/blob/master/build/elasticsearch/elasticsearch.yml 4 | # 5 | cluster.name: "docker-cluster" 6 | network.host: 0.0.0.0 7 | 8 | # minimum_master_nodes need to be explicitly set when bound on a public IP 9 | # set to 1 to allow single node clusters 10 | # Details: https://github.com/elastic/elasticsearch/pull/17288 11 | discovery.zen.minimum_master_nodes: 1 12 | 13 | ## Use single node discovery in order to disable production mode and avoid bootstrap checks 14 | ## see https://www.elastic.co/guide/en/elasticsearch/reference/current/bootstrap-checks.html 15 | # 16 | discovery.type: single-node 17 | -------------------------------------------------------------------------------- /server/src/app/services/repository/AbstractRepository.ts: -------------------------------------------------------------------------------- 1 | type Order = "DESC" | "ASC"; 2 | 3 | abstract class AbstractRepository { 4 | abstract create(T); 5 | abstract findOne(T); 6 | abstract findAll(order?: Order); 7 | abstract findAllByPaging(T, offset?: number, limit?: number, order?: Order); 8 | abstract findAllByIds(ids: number[], order?: Order); 9 | abstract update(T); 10 | abstract delete(T); 11 | 12 | getUniqueCriteria(T, colums: string[]): any[] { 13 | const params: any[] = []; 14 | colums.forEach(colum => { 15 | if (T[colum]) { 16 | const data = {}; 17 | data[colum] = T[colum]; 18 | params.push(data); 19 | } 20 | }); 21 | return params; 22 | } 23 | } 24 | 25 | export { AbstractRepository, Order }; 26 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": ["webpack-env", "jest"], 15 | "paths": { 16 | "@/*": ["src/*"] 17 | }, 18 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 19 | }, 20 | "include": [ 21 | "src/**/*.ts", 22 | "src/**/*.tsx", 23 | "src/**/*.vue", 24 | "tests/**/*.ts", 25 | "tests/**/*.tsx", 26 | "src/store/modules/portfolio.js" 27 | ], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /docker-elk/extensions/logspout/README.md: -------------------------------------------------------------------------------- 1 | # Logspout extension 2 | 3 | Logspout collects all Docker logs using the Docker logs API, and forwards them to Logstash without any additional 4 | configuration. 5 | 6 | ## Usage 7 | 8 | If you want to include the Logspout extension, run Docker Compose from the root of the repository with an additional 9 | command line argument referencing the `logspout-compose.yml` file: 10 | 11 | ```bash 12 | $ docker-compose -f docker-compose.yml -f extensions/logspout/logspout-compose.yml up 13 | ``` 14 | 15 | In your Logstash pipeline configuration, enable the `udp` input and set the input codec to `json`: 16 | 17 | ``` 18 | input { 19 | udp { 20 | port => 5000 21 | codec => json 22 | } 23 | } 24 | ``` 25 | 26 | ## Documentation 27 | 28 | https://github.com/looplab/logspout-logstash 29 | -------------------------------------------------------------------------------- /client/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "jsx", "json", "vue", "ts", "tsx"], 3 | transform: { 4 | "^.+\\.vue$": "vue-jest", 5 | ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": 6 | "jest-transform-stub", 7 | "^.+\\.tsx?$": "ts-jest" 8 | }, 9 | transformIgnorePatterns: ["/node_modules/"], 10 | moduleNameMapper: { 11 | "^@/(.*)$": "/src/$1" 12 | }, 13 | snapshotSerializers: ["jest-serializer-vue"], 14 | testMatch: [ 15 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" 16 | ], 17 | testURL: "http://localhost/", 18 | watchPlugins: [ 19 | "jest-watch-typeahead/filename", 20 | "jest-watch-typeahead/testname" 21 | ], 22 | globals: { 23 | "ts-jest": { 24 | babelConfig: true 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /server/src/config/logger/index.ts: -------------------------------------------------------------------------------- 1 | import winston from "winston"; 2 | 3 | const LOG_DIR = `${__dirname}/logs`; 4 | const tsFormat = () => new Date().toLocaleTimeString(); 5 | 6 | const logger = winston.createLogger({ 7 | level: process.env.NODE_ENV !== "development" ? "debug" : "info", 8 | format: winston.format.json(), 9 | transports: [ 10 | new winston.transports.Console(), 11 | new winston.transports.File({ 12 | filename: `${LOG_DIR}/error.log`, 13 | level: "error", 14 | }), 15 | new winston.transports.File({ 16 | filename: `${LOG_DIR}/debug.log`, 17 | level: "debug", 18 | }), 19 | new winston.transports.File({ 20 | filename: `${LOG_DIR}/info.log`, 21 | level: "info", 22 | }), 23 | new winston.transports.File({ filename: `${LOG_DIR}/combined.log` }), 24 | ], 25 | }); 26 | 27 | export { logger }; 28 | -------------------------------------------------------------------------------- /client/src/components/HomeView.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /server/src/app/routes/graphql/query/BookQuery.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfigMap, GraphQLList, GraphQLNonNull } from "graphql"; 2 | import { GraphQLInt } from "graphql/type/scalars"; 3 | import { BookType } from "../type/index"; 4 | 5 | import { BookService } from "../../../services"; 6 | import { Book } from "../../../types"; 7 | 8 | const book_service = new BookService(); 9 | const BookQuery: GraphQLFieldConfigMap = { 10 | book: { 11 | type: BookType, 12 | args: { 13 | id: { type: new GraphQLNonNull(GraphQLInt) } 14 | }, 15 | async resolve(parent, { id }: Book, context, info) { 16 | return await book_service.findOne({ id }); 17 | } 18 | }, 19 | books: { 20 | type: new GraphQLList(BookType), 21 | async resolve(parent, args, context, info) { 22 | return await book_service.findAll("DESC"); 23 | } 24 | } 25 | }; 26 | 27 | export { BookQuery }; 28 | -------------------------------------------------------------------------------- /client/src/store/modules/stocks.js: -------------------------------------------------------------------------------- 1 | import stocks from "../../data/stocks"; 2 | 3 | const state = { 4 | stocks: [] 5 | }; 6 | 7 | const getters = { 8 | stocks: state => { 9 | return state.stocks; 10 | } 11 | }; 12 | 13 | // Can't Async 14 | const mutations = { 15 | SET_STOCKS(state, stocks) { 16 | state.stocks = stocks; 17 | }, 18 | RND_STOCKS(state) { 19 | state.stocks.forEach(stock => { 20 | stock.price = Math.round(stock.price * (1 + Math.random() - 0.5)); 21 | }); 22 | } 23 | }; 24 | 25 | // Can Async 26 | const actions = { 27 | buyStock: ({ commit }, order) => { 28 | commit("BUY_STOCK", order); 29 | }, 30 | initStocks: ({ commit }) => { 31 | commit("SET_STOCKS", stocks); 32 | }, 33 | randomizeStocks: ({ commit }) => { 34 | commit("RND_STOCKS"); 35 | } 36 | }; 37 | 38 | export default { 39 | state, 40 | getters, 41 | mutations, 42 | actions 43 | }; 44 | -------------------------------------------------------------------------------- /server/src/app/services/model/AuthorityModel.ts: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from "sequelize"; 2 | 3 | import { sequelize } from "../../../config/database"; 4 | 5 | class AuthorityModel extends Model {} 6 | 7 | AuthorityModel.init( 8 | { 9 | id: { 10 | type: Sequelize.BIGINT, 11 | primaryKey: true, 12 | autoIncrement: true 13 | }, 14 | name: { 15 | type: Sequelize.STRING, 16 | validate: { 17 | is: /^[가-힣a-zA-Z]{3,20}/g 18 | }, 19 | unique: true 20 | }, 21 | level: { 22 | type: Sequelize.INTEGER 23 | }, 24 | 25 | active: { 26 | type: Sequelize.BOOLEAN, 27 | defaultValue: true 28 | }, 29 | created_at: { 30 | type: Sequelize.DATE 31 | }, 32 | updated_at: { 33 | type: Sequelize.DATE 34 | }, 35 | deleted_at: { 36 | type: Sequelize.DATE 37 | } 38 | }, 39 | { sequelize, modelName: "authorities" } 40 | ); 41 | 42 | export { AuthorityModel }; 43 | -------------------------------------------------------------------------------- /server/src/app/routes/graphql/query/AuthorityQuery.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfigMap, GraphQLList } from "graphql"; 2 | import { GraphQLInt, GraphQLString } from "graphql/type/scalars"; 3 | import { UserType } from "../type/index"; 4 | 5 | import { AuthorityService } from "../../../services"; 6 | import { User } from "../../../types"; 7 | 8 | const authority_service = new AuthorityService(); 9 | const AuthorityQuery: GraphQLFieldConfigMap = { 10 | authority: { 11 | type: UserType, 12 | args: { 13 | id: { type: GraphQLInt }, 14 | name: { type: GraphQLString } 15 | }, 16 | async resolve(parent, { id, email, name }: User, context, info) { 17 | return await authority_service.findOne({ id, name }); 18 | } 19 | }, 20 | authorities: { 21 | type: new GraphQLList(UserType), 22 | async resolve(parent, args, context, info) { 23 | return await authority_service.findAll("DESC"); 24 | } 25 | } 26 | }; 27 | 28 | export { AuthorityQuery }; 29 | -------------------------------------------------------------------------------- /server/src/app/routes/graphql/query/DivisionQuery.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfigMap, GraphQLList } from "graphql"; 2 | import { GraphQLInt, GraphQLString } from "graphql/type/scalars"; 3 | import { DivisionType } from "../type/index"; 4 | 5 | import { DivisionService } from "../../../services"; 6 | import { Division } from "../../../types"; 7 | 8 | const division_service = new DivisionService(); 9 | const DivisionQuery: GraphQLFieldConfigMap = { 10 | division: { 11 | type: DivisionType, 12 | args: { 13 | id: { type: GraphQLInt }, 14 | name: { type: GraphQLString } 15 | }, 16 | async resolve(parent, { id, name }: Division, context, info) { 17 | return await division_service.findOne({ id, name }); 18 | } 19 | }, 20 | divisions: { 21 | type: new GraphQLList(DivisionType), 22 | async resolve(parent, args, context, info) { 23 | return await division_service.findAll("DESC"); 24 | } 25 | } 26 | }; 27 | 28 | export { DivisionQuery }; 29 | -------------------------------------------------------------------------------- /server/src/app/routes/graphql/query/UserQuery.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfigMap, GraphQLList } from "graphql"; 2 | import { GraphQLInt, GraphQLString } from "graphql/type/scalars"; 3 | import { User } from "app/types"; 4 | 5 | import { UserService } from "../../../services"; 6 | import { UserType } from "../type"; 7 | 8 | const user_service = new UserService(); 9 | const UserQuery: GraphQLFieldConfigMap = { 10 | user: { 11 | type: UserType, 12 | args: { 13 | id: { type: GraphQLInt }, 14 | email: { type: GraphQLString }, 15 | name: { type: GraphQLString } 16 | }, 17 | async resolve(parent, { id, email, name }: User, context, info) { 18 | const db_user = await user_service.findOne({ id, email, name }); 19 | return db_user; 20 | } 21 | }, 22 | users: { 23 | type: new GraphQLList(UserType), 24 | async resolve(parent, args, context, info) { 25 | const users = await user_service.findAll("DESC"); 26 | return users; 27 | } 28 | } 29 | }; 30 | 31 | export { UserQuery }; 32 | -------------------------------------------------------------------------------- /server/src/app/routes/auth/AuthenticationRouter.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import { passport } from '../../../config/auth/passport'; 4 | 5 | const auth_router = Router(); 6 | auth_router.get('/', (req, res) => { 7 | res.send('Hello Authentication Rotuer'); 8 | }); 9 | 10 | auth_router.get('/github', passport.authenticate('github', { scope: [ 'user:email' ] })); 11 | 12 | auth_router.get('/github/callback', passport.authenticate('github', { failureRedirect: '/login' }), (req, res) => { 13 | res.redirect('/'); 14 | }); 15 | 16 | auth_router.get('/google', passport.authenticate('github', { scope: [ 'profile', 'email' ] })); 17 | 18 | auth_router.get('/google/callback', passport.authenticate('google', { failureRedirect: '/login' }), (req, res) => { 19 | res.redirect('/'); 20 | }); 21 | 22 | // Manual Log-In & Singn-Up 23 | auth_router.post('/login', (req, res) => { 24 | res.send(req.user); 25 | }); 26 | 27 | auth_router.get('/logout', (req, res) => { 28 | req.logout(); 29 | res.send(req.user); 30 | }); 31 | 32 | export { auth_router }; 33 | -------------------------------------------------------------------------------- /server/src/app/services/model/BookModel.ts: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from "sequelize"; 2 | 3 | import { sequelize } from "../../../config/database"; 4 | 5 | class BookModel extends Model {} 6 | BookModel.init( 7 | { 8 | id: { 9 | type: Sequelize.BIGINT, 10 | primaryKey: true, 11 | autoIncrement: true 12 | }, 13 | name: { 14 | type: Sequelize.STRING 15 | }, 16 | author: { 17 | type: Sequelize.STRING 18 | }, 19 | status: { 20 | type: Sequelize.STRING, 21 | comment: "GONE = 0, REQUESTED = 1, ORDERED = 2, NORMAR = 3, BORROWED = 4" 22 | }, 23 | description: { 24 | type: Sequelize.STRING 25 | }, 26 | 27 | active: { 28 | type: Sequelize.BOOLEAN, 29 | defaultValue: true 30 | }, 31 | created_at: { 32 | type: Sequelize.DATE 33 | }, 34 | updated_at: { 35 | type: Sequelize.DATE 36 | }, 37 | deleted_at: { 38 | type: Sequelize.DATE 39 | } 40 | }, 41 | { sequelize, modelName: "books", comment: "Book Table" } 42 | ); 43 | 44 | export { BookModel }; 45 | -------------------------------------------------------------------------------- /client/src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import { register } from "register-service-worker"; 4 | 5 | if (process.env.NODE_ENV === "production") { 6 | register(`${process.env.BASE_URL}service-worker.js`, { 7 | ready() { 8 | console.log( 9 | "App is being served from cache by a service worker.\n" + 10 | "For more details, visit https://goo.gl/AFskqB" 11 | ); 12 | }, 13 | registered() { 14 | console.log("Service worker has been registered."); 15 | }, 16 | cached() { 17 | console.log("Content has been cached for offline use."); 18 | }, 19 | updatefound() { 20 | console.log("New content is downloading."); 21 | }, 22 | updated() { 23 | console.log("New content is available; please refresh."); 24 | }, 25 | offline() { 26 | console.log( 27 | "No internet connection found. App is running in offline mode." 28 | ); 29 | }, 30 | error(error) { 31 | console.error("Error during service worker registration:", error); 32 | } 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /server/src/app/services/model/DivisionModel.ts: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from "sequelize"; 2 | import { sequelize } from "../../../config/database"; 3 | 4 | import { UserModel } from "./UserModel"; 5 | 6 | class DivisionModel extends Model {} 7 | DivisionModel.init( 8 | { 9 | id: { 10 | type: Sequelize.BIGINT, 11 | primaryKey: true, 12 | autoIncrement: true 13 | }, 14 | name: { 15 | type: Sequelize.STRING, 16 | unique: true, 17 | validate: { 18 | is: /^[가-힣a-zA-Z0-9]{2,20}/g 19 | } 20 | }, 21 | description: { 22 | type: Sequelize.STRING 23 | }, 24 | 25 | active: { 26 | type: Sequelize.BOOLEAN, 27 | defaultValue: true 28 | }, 29 | created_at: { 30 | type: Sequelize.DATE 31 | }, 32 | updated_at: { 33 | type: Sequelize.DATE 34 | }, 35 | deleted_at: { 36 | type: Sequelize.DATE 37 | } 38 | }, 39 | { sequelize, modelName: "divisions", comment: "Division Table" } 40 | ); 41 | 42 | DivisionModel.hasMany(UserModel, { as: "divisions" }); 43 | 44 | export { DivisionModel }; 45 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Book Management System. 2 | 3 | - Author : [Seolhun](https://github.com/Seolhun) 4 | - Date : 2017.10.19 5 | 6 | ## Used Stacks 7 | 8 | 1. `TypeScript` 9 | 2. `NodeJS`, `Express` 10 | 3. `Vue-Cli` 11 | 4. `GraphQL` 12 | 5. `Apollo Client` 13 | 6. `Sequelize` 14 | 15 | ## How to run 16 | 17 | #### `Server` 18 | 19 | - `npm install -g ts-node vue-cli typescript` 20 | 21 | 1. `yarn dev` 22 | 2. [http://localhost:4000/graphql](http://localhost:4000/graphql) 23 | 24 | #### `DB` 25 | 26 | 1. Create Default database using Raw SQL. 27 | 28 | - `/server/db/default.sql` 29 | 30 | 2. Set Database configuration. 31 | 32 | - `/server/src/config/database/index.ts` 33 | 34 | 3. Set `Sync` Database config 35 | 36 | - `sequelize.sync()` - create & update 37 | - `sequelize.sync({force: true})` - create & drop 38 | 39 | ## Reference 40 | 41 | - `Server` 42 | - [NodeJS - Express](http://expressjs.com/) 43 | - [Express Session](https://github.com/expressjs/session#options) 44 | - [GraphQL](http://graphql.org/learn/) 45 | - [Express GraphQL](https://github.com/graphql/express-graphql) 46 | - [Sequelize](http://docs.sequelizejs.com/) 47 | -------------------------------------------------------------------------------- /docker-elk/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: generic 4 | 5 | env: 6 | - DOCKER_COMPOSE_VERSION=1.14.0 7 | 8 | install: 9 | # Installing docker 10 | - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 11 | - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 12 | - sudo apt-get update 13 | - sudo apt-get -y install docker-ce 14 | 15 | # Installing docker-compose 16 | - sudo rm /usr/local/bin/docker-compose 17 | - curl -L "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" > docker-compose 18 | - chmod +x docker-compose 19 | - sudo mv docker-compose /usr/local/bin 20 | 21 | before_script: 22 | # Check docker & docker-compose versions 23 | - docker --version 24 | - docker-compose --version 25 | 26 | script: 27 | - docker-compose build 28 | - docker-compose up -d 29 | 30 | # Verifications 31 | - sleep 30 32 | - docker-compose logs 33 | - curl --retry 10 --retry-delay 5 -D- http://localhost:9200/ 34 | - curl --retry 10 --retry-delay 5 -D- http://localhost:5601/api/status 35 | -------------------------------------------------------------------------------- /client/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex, { ActionTree, MutationTree, Store } from "vuex"; 3 | 4 | Vue.use(Vuex); 5 | interface State { 6 | login: boolean; 7 | postUser: boolean; 8 | postOption: boolean; 9 | } 10 | 11 | const state: State = { 12 | login: false, 13 | postUser: false, 14 | postOption: false 15 | }; 16 | 17 | const actions: ActionTree = { 18 | async initAuth({ commit }): Promise { 19 | console.log(commit); 20 | } 21 | }; 22 | 23 | const mutations: MutationTree = { 24 | USER_LOGINING(state: State): void { 25 | state.login = true; 26 | }, 27 | 28 | USER_LOGINING_FINAL(state: State): void { 29 | state.login = false; 30 | }, 31 | 32 | POST_USER_INFO(state: State): void { 33 | state.postUser = true; 34 | }, 35 | 36 | POST_USER_FINAL(state: State): void { 37 | state.postUser = false; 38 | }, 39 | 40 | POST_OPTION_INFO(state: State): void { 41 | state.postOption = true; 42 | }, 43 | 44 | POST_OPTION_FINAL(state: State): void { 45 | state.postOption = false; 46 | } 47 | }; 48 | 49 | export default new Store({ 50 | state, 51 | actions, 52 | mutations 53 | }); 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | .DS_Store 11 | .idea 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Typescript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dist 63 | dist 64 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | .DS_Store 11 | .idea 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Typescript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /client/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | 3 | import VueI18n from "vue-i18n"; 4 | import messages from "./assets/languages/messages"; 5 | 6 | import axios from "axios"; 7 | import router from "./router"; 8 | 9 | import store from "./store"; 10 | 11 | import { InMemoryCache } from "apollo-cache-inmemory"; 12 | import { ApolloClient } from "apollo-client"; 13 | import { HttpLink } from "apollo-link-http"; 14 | import VueApollo from "vue-apollo"; 15 | 16 | import BootstrapVue from "bootstrap-vue"; 17 | import "bootstrap/dist/css/bootstrap.css"; 18 | 19 | import App from "./App.vue"; 20 | 21 | Vue.prototype.$appName = "Hi-Cord"; 22 | Vue.prototype.$http = axios; 23 | 24 | Vue.use(VueI18n); 25 | const i18n = new VueI18n({ 26 | locale: "ko", 27 | fallbackLocale: "en", 28 | messages, 29 | silentTranslationWarn: true 30 | }); 31 | 32 | const link = new HttpLink({ 33 | uri: `http://localhost:4000/graphql` 34 | }); 35 | const apolloClient = new ApolloClient({ 36 | link, 37 | cache: new InMemoryCache(), 38 | connectToDevTools: true 39 | }); 40 | const apolloProvider = new VueApollo({ 41 | defaultClient: apolloClient 42 | }); 43 | 44 | Vue.use(VueApollo); 45 | Vue.use(BootstrapVue); 46 | new Vue({ 47 | el: "#app", 48 | router, 49 | store, 50 | i18n, 51 | apolloProvider, 52 | render: h => h(App) 53 | }); 54 | -------------------------------------------------------------------------------- /server/src/config/environments/index.ts: -------------------------------------------------------------------------------- 1 | import DEV from "./develop"; 2 | import PROD from "./production"; 3 | import TEST from "./test"; 4 | 5 | interface SettingEnvironments { 6 | NODE_ENV: string; 7 | EXPRESS_PORT: number; 8 | 9 | // Session Key 10 | COOKIE_SESSION_MAX_AGE: number; 11 | COOKIE_SESSION_KEYS: string[]; 12 | SESSION_MAX_AGE: number; 13 | SESSION_KEYS: string[]; 14 | 15 | // Oauth2 16 | FACEBOOK_CLIENT_ID?: string; 17 | FACEBOOK_CLIENT_SECRET_ID?: string; 18 | GITHUB_CLIENT_ID?: string; 19 | GITHUB_CLIENT_SECRET_ID?: string; 20 | GOOGLE_CLIENT_ID?: string; 21 | GOOGLE_CLIENT_SECRET_ID?: string; 22 | } 23 | 24 | class Config { 25 | static setConfiguration(): SettingEnvironments { 26 | let env; 27 | if (process.env.NODE_ENV === "producton") { 28 | process.env.NODE_ENV = PROD.NODE_ENV; 29 | env = PROD; 30 | } else if (process.env.NODE_ENV === "test") { 31 | process.env.NODE_ENV = TEST.NODE_ENV; 32 | env = TEST; 33 | } else { 34 | process.env.NODE_ENV = DEV.NODE_ENV; 35 | env = DEV; 36 | } 37 | console.log("===============config/index.ts====================="); 38 | console.log(`process.env.NODE_ENV : ${process.env.NODE_ENV}`); 39 | console.log("===================================="); 40 | return env; 41 | } 42 | } 43 | 44 | export { Config, SettingEnvironments }; 45 | -------------------------------------------------------------------------------- /server/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "interface-name": { 9 | "options": [ 10 | "never-prefix" 11 | ] 12 | }, 13 | "max-classes-per-file": { 14 | "options": [ 15 | 10 16 | ] 17 | }, 18 | "max-line-length": { 19 | "options": [ 20 | 400 21 | ] 22 | }, 23 | "member-access": false, 24 | "member-ordering": false, 25 | "no-console": false, 26 | "no-empty-interface": false, 27 | "no-reference": false, 28 | "no-var-requires": false, 29 | "no-shadowed-variable": [ 30 | false, 31 | { 32 | "class": false, 33 | "enum": false, 34 | "function": false, 35 | "interface": false, 36 | "namespace": false, 37 | "typeAlias": false, 38 | "typeParameter": false 39 | } 40 | ], 41 | "quotemark": { 42 | "options": [ 43 | "single", 44 | "jsx-single", 45 | "avoid-escape" 46 | ] 47 | }, 48 | "variable-name": { 49 | "options": [ 50 | "ban-keywords", 51 | "check-format", 52 | "allow-pascal-case", 53 | "allow-snake-case" 54 | ] 55 | }, 56 | "object-literal-sort-keys": false 57 | }, 58 | "rulesDirectory": [] 59 | } 60 | -------------------------------------------------------------------------------- /docker-elk/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | web: 4 | build: . 5 | volumes: 6 | - ../server/:app 7 | ports: 8 | - "8080:8080" 9 | links: 10 | - redis 11 | redis: 12 | image: redis 13 | ports: 14 | - "6379:6379" 15 | 16 | # https://github.com/deviantony/docker-elk 17 | elasticsearch: 18 | build: 19 | context: elasticsearch/ 20 | volumes: 21 | - ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro 22 | ports: 23 | - "9200:9200" 24 | - "9300:9300" 25 | environment: 26 | ES_JAVA_OPTS: "-Xmx256m -Xms256m" 27 | networks: 28 | - elk 29 | 30 | logstash: 31 | build: 32 | context: logstash/ 33 | volumes: 34 | - ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml:ro 35 | - ./logstash/pipeline:/usr/share/logstash/pipeline:ro 36 | ports: 37 | - "5000:5000" 38 | environment: 39 | LS_JAVA_OPTS: "-Xmx256m -Xms256m" 40 | networks: 41 | - elk 42 | depends_on: 43 | - elasticsearch 44 | 45 | kibana: 46 | build: 47 | context: kibana/ 48 | volumes: 49 | - ./kibana/config/:/usr/share/kibana/config:ro 50 | ports: 51 | - "5601:5601" 52 | networks: 53 | - elk 54 | depends_on: 55 | - elasticsearch 56 | 57 | networks: 58 | elk: 59 | driver: bridge 60 | -------------------------------------------------------------------------------- /server/src/app/services/model/UserAuthorityModel.ts: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from "sequelize"; 2 | import { sequelize } from "../../../config/database"; 3 | 4 | import { AuthorityModel } from "./AuthorityModel"; 5 | import { UserModel } from "./UserModel"; 6 | 7 | class UserAuthorityModel extends Model {} 8 | 9 | UserAuthorityModel.init( 10 | { 11 | id: { 12 | type: Sequelize.BIGINT, 13 | primaryKey: true, 14 | autoIncrement: true 15 | }, 16 | user_id: { 17 | type: Sequelize.BIGINT, 18 | unique: "uk_user_authorities" 19 | }, 20 | authority_id: { 21 | type: Sequelize.BIGINT, 22 | unique: "uk_user_authorities" 23 | }, 24 | 25 | active: { 26 | type: Sequelize.BOOLEAN, 27 | defaultValue: true 28 | }, 29 | created_at: { 30 | type: Sequelize.DATE 31 | }, 32 | updated_at: { 33 | type: Sequelize.DATE 34 | }, 35 | deleted_at: { 36 | type: Sequelize.DATE 37 | } 38 | }, 39 | { 40 | sequelize, 41 | modelName: "user_authorities", 42 | comment: "Uesr Authorities Table" 43 | } 44 | ); 45 | 46 | AuthorityModel.belongsToMany(UserModel, { 47 | as: "user", 48 | through: { 49 | model: UserAuthorityModel 50 | }, 51 | foreignKey: "authority_id" 52 | }); 53 | 54 | UserModel.belongsToMany(AuthorityModel, { 55 | as: "authority", 56 | through: { 57 | model: UserAuthorityModel 58 | }, 59 | foreignKey: "user_id" 60 | }); 61 | 62 | export { UserAuthorityModel }; 63 | -------------------------------------------------------------------------------- /server/src/app/services/model/UserModel.ts: -------------------------------------------------------------------------------- 1 | import Sequelize, { Model } from "sequelize"; 2 | import { sequelize } from "../../../config/database"; 3 | 4 | class UserModel extends Model {} 5 | 6 | UserModel.init( 7 | { 8 | id: { 9 | type: Sequelize.BIGINT, 10 | primaryKey: true, 11 | autoIncrement: true 12 | }, 13 | name: { 14 | type: Sequelize.STRING, 15 | unique: true, 16 | validate: { 17 | is: /^[가-힣a-zA-Z]{3,20}/g 18 | } 19 | }, 20 | email: { 21 | type: Sequelize.STRING, 22 | unique: true, 23 | validate: { 24 | isEmail: true 25 | } 26 | }, 27 | 28 | password: { 29 | type: Sequelize.STRING 30 | }, 31 | birth: { 32 | type: Sequelize.DATEONLY 33 | }, 34 | 35 | google_id: { 36 | type: Sequelize.STRING, 37 | unique: true 38 | }, 39 | github_id: { 40 | type: Sequelize.STRING, 41 | unique: true 42 | }, 43 | facebook_id: { 44 | type: Sequelize.STRING, 45 | unique: true 46 | }, 47 | 48 | active: { 49 | type: Sequelize.BOOLEAN, 50 | defaultValue: true 51 | }, 52 | created_at: { 53 | type: Sequelize.DATE 54 | }, 55 | updated_at: { 56 | type: Sequelize.DATE 57 | }, 58 | deleted_at: { 59 | type: Sequelize.DATE 60 | } 61 | }, 62 | { 63 | sequelize, 64 | modelName: "users", 65 | comment: "Uesr Table" 66 | } 67 | ); 68 | 69 | export { UserModel }; 70 | -------------------------------------------------------------------------------- /client/src/components/ui/divisions/DivisionDetailView.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 56 | -------------------------------------------------------------------------------- /client/src/store/modules/portfolio.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | funds: 10000, 3 | stocks: [] 4 | }; 5 | 6 | // Can't Async 7 | const mutations = { 8 | BUY_STOCK(state, { stockId, quantity, stockPrice }) { 9 | const record = state.stocks.find(element => element.id === stockId); 10 | if (record) { 11 | record.quantity += quantity; 12 | } else { 13 | state.stocks.push({ 14 | id: stockId, 15 | quantity: quantity 16 | }); 17 | } 18 | state.funds -= stockPrice * quantity; 19 | }, 20 | SELL_STOCK(state, { stockId, quantity, stockPrice }) { 21 | const record = state.stocks.find(element => element.id === stockId); 22 | if (record.quantity > quantity) { 23 | record.quantity -= quantity; 24 | } else { 25 | state.stocks.splice(state.stocks.indexOf(record), 1); 26 | } 27 | state.funds += stockPrice * quantity; 28 | } 29 | }; 30 | 31 | // Can Async 32 | const actions = { 33 | sellStock: ({ commit }, order) => { 34 | commit("SELL_STOCK", order); 35 | } 36 | }; 37 | 38 | const getters = { 39 | stockPortfolio(state, getters) { 40 | return state.stocks.map(stock => { 41 | const record = getters.stocks.find(element => element.id === stock.id); 42 | return { 43 | id: stock.id, 44 | quantity: stock.quantity, 45 | name: record.name, 46 | price: record.price 47 | }; 48 | }); 49 | }, 50 | 51 | funds(state) { 52 | return state.funds; 53 | } 54 | }; 55 | 56 | export default { 57 | state, 58 | getters, 59 | mutations, 60 | actions 61 | }; 62 | -------------------------------------------------------------------------------- /server/src/app/routes/graphql/mutation/DivisionMutation.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfigMap, GraphQLNonNull } from "graphql"; 2 | import { GraphQLInt, GraphQLString } from "graphql/type/scalars"; 3 | import { BookType, DivisionType, UserType } from "../type/index"; 4 | 5 | import { DivisionService } from "../../../services"; 6 | import { Division } from "../../../types"; 7 | 8 | const division_service = new DivisionService(); 9 | const DivisionMutation: GraphQLFieldConfigMap = { 10 | addDivision: { 11 | type: DivisionType, 12 | args: { 13 | name: { type: new GraphQLNonNull(GraphQLString) }, 14 | description: { type: GraphQLString } 15 | }, 16 | async resolve(parent, { name, description }: Division, context, info) { 17 | return await division_service.createdDivision({ name, description }); 18 | } 19 | }, 20 | editDivision: { 21 | type: DivisionType, 22 | args: { 23 | id: { type: GraphQLInt }, 24 | name: { type: new GraphQLNonNull(GraphQLString) }, 25 | description: { type: new GraphQLNonNull(GraphQLString) } 26 | }, 27 | async resolve(parent, { id, name, description }: Division, context, info) { 28 | return await division_service.updatedDivision({ name, description }); 29 | } 30 | }, 31 | deleteDivision: { 32 | type: DivisionType, 33 | args: { 34 | id: { type: GraphQLInt }, 35 | name: { type: GraphQLString } 36 | }, 37 | async resolve(parent, { id, name }: Division, context, info) { 38 | return await division_service.deletedDivision({ id }); 39 | } 40 | } 41 | }; 42 | 43 | export { DivisionMutation }; 44 | -------------------------------------------------------------------------------- /server/src/app/types/index.ts: -------------------------------------------------------------------------------- 1 | interface Authority { 2 | id?: number; 3 | name?: string; 4 | level?: number; 5 | description?: string; 6 | 7 | active?: boolean; 8 | created_at?: string; 9 | updated_at?: string; 10 | deleted_at?: string; 11 | } 12 | 13 | interface Division { 14 | id?: number; 15 | name?: string; 16 | description?: string; 17 | 18 | active?: boolean; 19 | created_at?: string; 20 | updated_at?: string; 21 | deleted_at?: string; 22 | } 23 | 24 | interface Book { 25 | id?: number; 26 | name?: string; 27 | author?: string; 28 | status?: BookStatus; 29 | description?: string; 30 | 31 | active?: boolean; 32 | created_at?: string; 33 | updated_at?: string; 34 | deleted_at?: string; 35 | } 36 | 37 | enum BookStatus { 38 | GONE = 0, 39 | REQUESTED = 1, 40 | ORDERED = 2, 41 | NORMAL = 3, 42 | BORROWED = 4 43 | } 44 | 45 | interface User { 46 | id?: number; 47 | name?: string; 48 | email?: string; 49 | password?: string; 50 | birth?: string; 51 | 52 | division_id?: number; 53 | division?: Division; 54 | 55 | google_id?: string; 56 | github_id?: string; 57 | facebook_id?: string; 58 | 59 | active?: boolean; 60 | created_at?: string; 61 | updated_at?: string; 62 | deleted_at?: string; 63 | } 64 | 65 | interface UserAuthority { 66 | id?: number; 67 | user_id?: number; 68 | user?: User; 69 | authority_id?: number; 70 | authority?: Authority; 71 | 72 | active?: boolean; 73 | created_at?: string; 74 | updated_at?: string; 75 | deleted_at?: string; 76 | } 77 | 78 | export { Authority, Book, BookStatus, Division, User, UserAuthority }; 79 | -------------------------------------------------------------------------------- /server/src/app/services/BookService.ts: -------------------------------------------------------------------------------- 1 | import { Book } from "../types"; 2 | import { BookRepository } from "./repository"; 3 | import { Order } from "./repository/AbstractRepository"; 4 | import { BookModel } from "./model"; 5 | 6 | const book_repository = new BookRepository(["id"]); 7 | class BookService { 8 | createdBook({ name, author, description }: Book): Promise { 9 | const book = book_repository.create({ name, author, description }); 10 | return book; 11 | } 12 | 13 | findOne({ id }: Book): Promise { 14 | if (!id) { 15 | return Promise.reject(new Error("id is requirement.")); 16 | } 17 | return book_repository.findOne({ id }); 18 | } 19 | 20 | findAll(order: Order): Promise { 21 | return book_repository.findAll(order); 22 | } 23 | 24 | updatedBook({ 25 | id, 26 | name, 27 | author, 28 | status, 29 | description 30 | }: Book): Promise { 31 | if (!id) { 32 | return Promise.reject(new Error("id is requirement.")); 33 | } 34 | const dbBook = book_repository.findOne({ id }); 35 | if (!dbBook) { 36 | return Promise.reject(new Error("The book is not found")); 37 | } 38 | return book_repository.update({ name, author, status, description }); 39 | } 40 | 41 | deletedBook({ id }: Book): Promise { 42 | if (!id) { 43 | return Promise.reject(new Error("id is requirement.")); 44 | } 45 | const dbBook = book_repository.findOne({ id }); 46 | if (!dbBook) { 47 | return Promise.reject(new Error("The division is not found")); 48 | } 49 | return book_repository.delete({ id }); 50 | } 51 | } 52 | 53 | export { BookService }; 54 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 62 | 63 | 71 | -------------------------------------------------------------------------------- /server/src/app/routes/graphql/mutation/AuthorityMutation.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfigMap, GraphQLNonNull } from "graphql"; 2 | import { GraphQLInt, GraphQLString } from "graphql/type/scalars"; 3 | import { AuthorityType } from "../type/index"; 4 | 5 | import { AuthorityService } from "../../../services"; 6 | import { Authority } from "../../../types"; 7 | 8 | const authority_service = new AuthorityService(); 9 | 10 | const AuthorityMutation: GraphQLFieldConfigMap = { 11 | addAuthority: { 12 | type: AuthorityType, 13 | args: { 14 | id: { type: GraphQLInt }, 15 | name: { type: new GraphQLNonNull(GraphQLString) }, 16 | level: { type: new GraphQLNonNull(GraphQLInt) } 17 | }, 18 | async resolve( 19 | parent, 20 | { name, level, description }: Authority, 21 | context, 22 | info 23 | ) { 24 | return await authority_service.createdAuthority({ 25 | name, 26 | level, 27 | description 28 | }); 29 | } 30 | }, 31 | editAuthority: { 32 | type: AuthorityType, 33 | args: { 34 | id: { type: GraphQLInt }, 35 | name: { type: GraphQLString }, 36 | level: { type: GraphQLInt } 37 | }, 38 | async resolve(parent, { name, level, description }: Authority) { 39 | return await authority_service.updatedAuthority({ 40 | name, 41 | level, 42 | description 43 | }); 44 | } 45 | }, 46 | deleteAuthority: { 47 | type: AuthorityType, 48 | args: { 49 | id: { type: GraphQLInt }, 50 | name: { type: GraphQLString } 51 | }, 52 | async resolve(parent, { id, name }: Authority) { 53 | return await authority_service.deletedAuthority({ id, name }); 54 | } 55 | } 56 | }; 57 | 58 | export { AuthorityMutation }; 59 | -------------------------------------------------------------------------------- /client/src/components/ui/users/UserDetailView.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 65 | -------------------------------------------------------------------------------- /client/src/components/ui/books/BookDetailView.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 66 | -------------------------------------------------------------------------------- /client/src/assets/languages/nation/ja.ts: -------------------------------------------------------------------------------- 1 | // a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z is alphabetized. 2 | // `this` is first order. 3 | const ja = { 4 | common: { 5 | webDev: "ウェブ開発者", 6 | birth: "생년월일", 7 | cancel: "취소", 8 | china: "中国", 9 | description: "설명", 10 | email: "이메일", 11 | form: { 12 | confirm: "확인", 13 | submit: "제출" 14 | }, 15 | id: "번호", 16 | japan: "日本の", 17 | korea: "大韓民国", 18 | name: "이름", 19 | nation: "国", 20 | password: "비밀번호", 21 | confirm_password: "비밀번호 확인", 22 | search: "검색", 23 | usa: "米国" 24 | }, 25 | authority: { 26 | this: "권한", 27 | authority_list: "권한 리스트", 28 | message: {}, 29 | placeholder: {} 30 | }, 31 | book: { 32 | this: "책", 33 | book_list: "책 리스트", 34 | message: {}, 35 | placeholder: {}, 36 | author: "저자" 37 | }, 38 | content: { 39 | label: { 40 | title: "제 목", 41 | contentType: "컨텐츠 종류", 42 | tags: "태 그", 43 | content: "내 용" 44 | }, 45 | message: { 46 | insertSuccess: "컨텐츠를 성공적으로 등록하였습니다." 47 | }, 48 | placeholder: { 49 | title: "컨텐츠의 제목을 입력해주세요.", 50 | tags: "블로그와 관련된 태그를 입력해주세요.", 51 | search: "검색어를 입력해주세요." 52 | } 53 | }, 54 | division: { 55 | this: "부서", 56 | division_list: "부서 리스트", 57 | message: {}, 58 | placeholder: {} 59 | }, 60 | login: { 61 | this: "Log-In", 62 | message: { 63 | not_user1: "회원이 아니신가요? 지금 ", 64 | not_user2: "하세요" 65 | }, 66 | placeholder: {} 67 | }, 68 | signin: { 69 | this: "회원가입", 70 | sns: { 71 | message: "SNS을 이용해서 가입하세요." 72 | }, 73 | message: {}, 74 | placeholder: {} 75 | }, 76 | user: { 77 | this: "사용자", 78 | user_list: "사용자 리스트", 79 | message: {}, 80 | placeholder: {} 81 | } 82 | }; 83 | export default ja; 84 | -------------------------------------------------------------------------------- /client/src/assets/languages/nation/ch.ts: -------------------------------------------------------------------------------- 1 | // a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z is alphabetized. 2 | // `this` is first order. 3 | const ch = { 4 | common: { 5 | webDev: "Web 開發人員", 6 | birth: "Birth", 7 | cancel: "Cancel", 8 | china: "中國", 9 | email: "E-Mail", 10 | description: "설명", 11 | form: { 12 | confirm: "Confirm", 13 | submit: "Submit" 14 | }, 15 | id: "No", 16 | japan: "日本", 17 | korea: "韓國", 18 | name: "이름", 19 | nation: "國家", 20 | password: "비밀번호", 21 | confirm_password: "비밀번호 확인", 22 | search: "Search", 23 | usa: "美國" 24 | }, 25 | authority: { 26 | this: "권한", 27 | authority_list: "권한 리스트", 28 | message: {}, 29 | placeholder: {} 30 | }, 31 | book: { 32 | this: "책", 33 | book_list: "책 리스트", 34 | message: {}, 35 | placeholder: {}, 36 | author: "저자" 37 | }, 38 | content: { 39 | label: { 40 | title: "제 목", 41 | contentType: "컨텐츠 종류", 42 | tags: "태 그", 43 | content: "내 용" 44 | }, 45 | message: { 46 | insertSuccess: "컨텐츠를 성공적으로 등록하였습니다." 47 | }, 48 | placeholder: { 49 | title: "컨텐츠의 제목을 입력해주세요.", 50 | tags: "블로그와 관련된 태그를 입력해주세요.", 51 | search: "검색어를 입력해주세요." 52 | } 53 | }, 54 | division: { 55 | this: "부서", 56 | division_list: "부서 리스트", 57 | message: {}, 58 | placeholder: {} 59 | }, 60 | login: { 61 | this: "Log-In", 62 | message: { 63 | not_user1: "회원이 아니신가요? 지금 ", 64 | not_user2: "하세요" 65 | }, 66 | placeholder: {} 67 | }, 68 | signin: { 69 | this: "회원가입", 70 | sns: { 71 | message: "SNS을 이용해서 가입하세요." 72 | }, 73 | message: {}, 74 | placeholder: {} 75 | }, 76 | user: { 77 | this: "사용자", 78 | user_list: "사용자 리스트", 79 | message: {}, 80 | placeholder: {} 81 | } 82 | }; 83 | export default ch; 84 | -------------------------------------------------------------------------------- /client/src/assets/languages/nation/ko.ts: -------------------------------------------------------------------------------- 1 | // a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z is alphabetized. 2 | // `this` is first order. 3 | const ko = { 4 | common: { 5 | webDev: "웹 개발자", 6 | birth: "생년월일", 7 | cancel: "취소", 8 | china: "중국", 9 | description: "설명", 10 | email: "이메일", 11 | form: { 12 | confirm: "확인", 13 | submit: "제출" 14 | }, 15 | id: "번호", 16 | japan: "일본", 17 | korea: "대한민국", 18 | name: "이름", 19 | nation: "국가", 20 | password: "비밀번호", 21 | confirm_password: "비밀번호 확인", 22 | search: "검색", 23 | status: "상태", 24 | usa: "미국" 25 | }, 26 | authority: { 27 | this: "권한", 28 | authority_list: "권한 리스트", 29 | message: {}, 30 | placeholder: {} 31 | }, 32 | book: { 33 | this: "책", 34 | book_list: "책 리스트", 35 | message: {}, 36 | placeholder: {}, 37 | author: "저자" 38 | }, 39 | content: { 40 | label: { 41 | title: "제 목", 42 | contentType: "컨텐츠 종류", 43 | tags: "태 그", 44 | content: "내 용" 45 | }, 46 | message: { 47 | insertSuccess: "컨텐츠를 성공적으로 등록하였습니다." 48 | }, 49 | placeholder: { 50 | title: "컨텐츠의 제목을 입력해주세요.", 51 | tags: "블로그와 관련된 태그를 입력해주세요.", 52 | search: "검색어를 입력해주세요." 53 | } 54 | }, 55 | division: { 56 | this: "부서", 57 | division_list: "부서 리스트", 58 | message: {}, 59 | placeholder: {} 60 | }, 61 | login: { 62 | this: "로그인", 63 | message: { 64 | not_user1: "회원이 아니신가요? 지금 ", 65 | not_user2: "하세요" 66 | }, 67 | placeholder: {} 68 | }, 69 | signin: { 70 | this: "회원가입", 71 | sns: { 72 | message: "SNS을 이용해서 가입하세요." 73 | }, 74 | message: {}, 75 | placeholder: {} 76 | }, 77 | user: { 78 | this: "사용자", 79 | user_list: "사용자 리스트", 80 | message: {}, 81 | placeholder: {} 82 | } 83 | }; 84 | export default ko; 85 | -------------------------------------------------------------------------------- /server/src/app/routes/graphql/mutation/BookMutation.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfigMap, GraphQLNonNull } from "graphql"; 2 | import { 3 | GraphQLBoolean, 4 | GraphQLInt, 5 | GraphQLString 6 | } from "graphql/type/scalars"; 7 | import { BookType } from "../type/index"; 8 | 9 | import { BookService } from "../../../services"; 10 | import { Book } from "../../../types"; 11 | 12 | const book_service = new BookService(); 13 | const BookMutation: GraphQLFieldConfigMap = { 14 | addUser: { 15 | type: BookType, 16 | args: { 17 | name: { type: new GraphQLNonNull(GraphQLString) }, 18 | author: { type: new GraphQLNonNull(GraphQLString) }, 19 | description: { type: new GraphQLNonNull(GraphQLInt) } 20 | }, 21 | async resolve(parent, { name, author, description }: Book, context, info) { 22 | return await book_service.createdBook({ name, author, description }); 23 | } 24 | }, 25 | editUser: { 26 | type: BookType, 27 | args: { 28 | id: { type: GraphQLInt }, 29 | name: { type: new GraphQLNonNull(GraphQLString) }, 30 | author: { type: new GraphQLNonNull(GraphQLString) }, 31 | status: { type: new GraphQLNonNull(GraphQLBoolean) }, 32 | description: { type: new GraphQLNonNull(GraphQLInt) } 33 | }, 34 | async resolve( 35 | parent, 36 | { id, name, author, status, description }: Book, 37 | context, 38 | info 39 | ) { 40 | return await book_service.updatedBook({ 41 | id, 42 | name, 43 | author, 44 | status, 45 | description 46 | }); 47 | } 48 | }, 49 | deleteUser: { 50 | type: BookType, 51 | args: { 52 | id: { type: GraphQLInt } 53 | }, 54 | async resolve(parent, { id }: Book, context, info) { 55 | return await book_service.deletedBook({ id }); 56 | } 57 | } 58 | }; 59 | 60 | export { BookMutation }; 61 | -------------------------------------------------------------------------------- /client/src/components/ui/divisions/DivisionsView.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 68 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-example", 3 | "version": "1.0.0", 4 | "description": "Graphql example", 5 | "main": "app.ts", 6 | "scripts": { 7 | "dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/app.ts", 8 | "build": "tsc", 9 | "start": "node dist/app.js", 10 | "test": "jest" 11 | }, 12 | "jest": { 13 | "transform": { 14 | "^.+\\.tsx?$": "ts-jest" 15 | }, 16 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 17 | "moduleFileExtensions": [ 18 | "ts", 19 | "tsx", 20 | "js", 21 | "jsx", 22 | "json", 23 | "node" 24 | ] 25 | }, 26 | "author": "seolhun", 27 | "license": "MIT", 28 | "dependencies": { 29 | "axios": "^0.18.0", 30 | "bcryptjs": "^2.4.3", 31 | "body-parser": "^1.18.2", 32 | "cookie-session": "^2.0.0-beta.3", 33 | "cors": "^2.8.4", 34 | "express": "^4.16.3", 35 | "express-graphql": "^0.7.1", 36 | "express-session": "^1.15.6", 37 | "graphql": "^14.2.0", 38 | "helmet": "^3.12.0", 39 | "jest": "^24.5.0", 40 | "lodash": "^4.17.5", 41 | "mariadb": "^2.1.0", 42 | "nodemon": "^2.0.0", 43 | "passport": "^0.4.0", 44 | "passport-facebook": "^3.0.0", 45 | "passport-github2": "^0.1.11", 46 | "passport-google-oauth20": "^2.0.0", 47 | "sequelize": "^5.2.3", 48 | "sinon": "^8.0.0", 49 | "winston": "^3.0.0-rc2" 50 | }, 51 | "devDependencies": { 52 | "@types/bcryptjs": "^2.4.2", 53 | "@types/express": "^4.11.1", 54 | "@types/express-graphql": "0.8.1", 55 | "@types/graphql": "^14.0.7", 56 | "@types/helmet": "0.0.45", 57 | "@types/jest": "^24.0.11", 58 | "@types/lodash": "^4.14.148", 59 | "@types/passport": "^1.0.0", 60 | "@types/sequelize": "^4.28.4", 61 | "@types/sinon": "^7.0.10", 62 | "dotenv": "^8.1.0", 63 | "ts-jest": "^24.1.0", 64 | "ts-node": "^8.3.0", 65 | "typescript": "^3.6.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /client/src/components/ui/users/UsersView.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 71 | -------------------------------------------------------------------------------- /client/src/assets/languages/nation/en.ts: -------------------------------------------------------------------------------- 1 | // a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z is alphabetized. 2 | // `this` is first order. 3 | const en = { 4 | common: { 5 | webDev: "Web Developer", 6 | birth: "Birth", 7 | cancel: "Cancel", 8 | china: "China", 9 | description: "Description", 10 | email: "E-Mail", 11 | form: { 12 | confirm: "Confirm", 13 | submit: "Submit" 14 | }, 15 | id: "No", 16 | japan: "Japan", 17 | korea: "Korea", 18 | name: "Name", 19 | nation: "Nation", 20 | password: "Password", 21 | confirm_password: "Confirm Password", 22 | search: "Search", 23 | status: "Status", 24 | usa: "USA" 25 | }, 26 | authority: { 27 | this: "Authority", 28 | authority_list: "Authority list", 29 | message: {}, 30 | placeholder: {} 31 | }, 32 | book: { 33 | this: "Book", 34 | book_list: "Book list", 35 | message: {}, 36 | placeholder: {}, 37 | author: "Author" 38 | }, 39 | content: { 40 | label: { 41 | title: "제 목", 42 | contentType: "컨텐츠 종류", 43 | tags: "태 그", 44 | content: "내 용" 45 | }, 46 | message: { 47 | insertSuccess: "컨텐츠를 성공적으로 등록하였습니다." 48 | }, 49 | placeholder: { 50 | title: "컨텐츠의 제목을 입력해주세요.", 51 | tags: "블로그와 관련된 태그를 입력해주세요.", 52 | search: "검색어를 입력해주세요." 53 | } 54 | }, 55 | division: { 56 | this: "Division", 57 | division_list: "Division list", 58 | message: {}, 59 | placeholder: {} 60 | }, 61 | login: { 62 | this: "Log-In", 63 | message: { 64 | not_user1: "Not yet a member?", 65 | not_user2: "now" 66 | }, 67 | placeholder: {} 68 | }, 69 | signin: { 70 | this: "Sign-In", 71 | sns: { 72 | message: "SNS을 이용해서 가입하세요." 73 | }, 74 | message: {}, 75 | placeholder: {} 76 | }, 77 | user: { 78 | this: "User", 79 | user_list: "User list", 80 | message: {}, 81 | placeholder: {} 82 | } 83 | }; 84 | export default en; 85 | -------------------------------------------------------------------------------- /server/src/app/services/AuthorityService.ts: -------------------------------------------------------------------------------- 1 | import { Authority } from "../types"; 2 | import { AuthorityRepository } from "./repository"; 3 | import { Order } from "./repository/AbstractRepository"; 4 | 5 | const authority_repository = new AuthorityRepository(["id", "name"]); 6 | class AuthorityService { 7 | createdAuthority = async ({ name, level }: Authority) => { 8 | if (!name || !level) { 9 | return Promise.reject(new Error("The name and level is requirement.")); 10 | } 11 | 12 | const dbAuthority = await authority_repository.findOne({ name }); 13 | if (dbAuthority) { 14 | return Promise.reject(new Error(`Already '${name}' is exists.`)); 15 | } 16 | return authority_repository.create({ name, level }); 17 | }; 18 | 19 | findOne = async ({ id, name }: Authority) => { 20 | if (!id && !name) { 21 | return Promise.reject(new Error("One of id and name is requirement.")); 22 | } 23 | return authority_repository.findOne({ id, name }); 24 | }; 25 | 26 | findAll = async (order: Order) => { 27 | return authority_repository.findAll("DESC"); 28 | }; 29 | 30 | updatedAuthority = async ({ name, level, description }: Authority) => { 31 | if (!name) { 32 | return Promise.reject(new Error("id or name is requirement.")); 33 | } 34 | 35 | const dbAuthority = await authority_repository.findOne({ name }); 36 | if (!dbAuthority) { 37 | return Promise.reject(new Error("The authority is not found")); 38 | } 39 | return authority_repository.update({ name, level, description }); 40 | }; 41 | 42 | deletedAuthority = async ({ id, name }: Authority) => { 43 | if (!id && !name) { 44 | return Promise.reject(new Error(`id or name is requirement.`)); 45 | } 46 | const dbAuthority = await authority_repository.findOne({ id, name }); 47 | 48 | if (!dbAuthority) { 49 | return Promise.reject(new Error("The authority is not found")); 50 | } 51 | return authority_repository.delete({ id, name }); 52 | }; 53 | } 54 | 55 | export { AuthorityService }; 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Waffle.io - Columns and their card count](https://badge.waffle.io/Seolhun/vue-type-graphql-example.png?columns=all)](https://waffle.io/Seolhun/vue-type-graphql-example?utm_source=badge) [![Greenkeeper badge](https://badges.greenkeeper.io/Seolhun/vue-type-graphql-example.svg)](https://greenkeeper.io/) 2 | 3 | # Book Management System. 4 | 5 | - Author : [Seolhun](https://github.com/Seolhun) 6 | - Date : 2017.10.19 7 | 8 | ## Used Stacks 9 | 10 | 1. `TypeScript` 11 | 2. `NodeJS`, `Express` 12 | 3. `Vue-Cli` 13 | 4. `GraphQL` 14 | 5. `Apollo Client` 15 | 6. `Sequelize` 16 | 17 | ## Commit Convention 18 | 19 | - Server : [Server] 20 | - Client : [Cient] 21 | - Contents 22 | - GraphQL 23 | - Vue 24 | - Test 25 | - ... 26 | 27 | ## How to run 28 | 29 | #### Server 30 | 31 | ```bash 32 | $ cd server 33 | $ yarn install 34 | $ yarn dev 35 | ``` 36 | 37 | - Docker 38 | 39 | ```bash 40 | $ yarn install 41 | $ docker-compose up -d 42 | ``` 43 | 44 | - NPM scripts 45 | 46 | ```bash 47 | $ yarn dev 48 | ``` 49 | 50 | > [http://localhost:4000/graphql](http://localhost:4000/graphql) 51 | 52 | #### DB 53 | 54 | 1.Create Default database using Raw SQL. 55 | 56 | - `/src/config/db/default.sql` 57 | 58 | 1. Set Database configuration. 59 | 60 | - `/server/src/config/database/index.ts` 61 | 62 | 3. Set `Sync` Database config 63 | 64 | - `sequelize.sync()` - create & update 65 | - `sequelize.sync({force: true})` - create & drop 66 | 67 | --- 68 | 69 | #### Client 70 | 71 | ```bash 72 | $ cd client 73 | $ yarn install 74 | $ yarn dev 75 | ``` 76 | 77 | > [http://localhost:7000/](http://localhost:7000/) 78 | 79 | ## Reference 80 | 81 | - `Server` 82 | 83 | - [NodeJS - Express](http://expressjs.com/) 84 | - [Express Session](https://github.com/expressjs/session#options) 85 | - [GraphQL](http://graphql.org/learn/) 86 | - [Express GraphQL](https://github.com/graphql/express-graphql) 87 | - [Sequelize](http://docs.sequelizejs.com/) 88 | 89 | - `Client` 90 | - [Vue](https://vuejs.org/) 91 | - [Vue Apollo Client](https://github.com/akryum/vue-apollo) 92 | -------------------------------------------------------------------------------- /client/src/utils/Validators.ts: -------------------------------------------------------------------------------- 1 | import validator from "validator"; 2 | 3 | const number: RegExp = /[0-9]+/; 4 | const upper_alphabet: RegExp = /[A-Z]+/; 5 | const lower_alphabet: RegExp = /[a-z]+/; 6 | const korean: RegExp = /[가-힣]+/; 7 | const special_characters: RegExp = /[\@\!\#\$\%\^\&\*\(\)\_\+\=\-\.\,\>\<\?\/\`\~\"\:\'\;\\]*/; 8 | const special_character: string = "`~!@#$%^&*()_-=+.>, { 16 | if (email.length < 4) { 17 | return { result: false, msg: "Requirement email" }; 18 | } else if (!validator.isEmail(email)) { 19 | return { 20 | result: false, 21 | msg: `${email} is not valid. Input the right Email.` 22 | }; 23 | } 24 | return { result: true, msg: "" }; 25 | }; 26 | 27 | export const isName = (name: string): ValidationResponse => { 28 | if (name.length < 2) { 29 | return { result: false, msg: "Requirement over 2 characters" }; 30 | } else if (korean.test(name)) { 31 | return { result: false, msg: "Never use Korean" }; 32 | } else if (!special_characters.test(name)) { 33 | return { 34 | result: false, 35 | msg: `Requirement lower special_characters(${special_character})` 36 | }; 37 | } 38 | return { result: true, msg: "" }; 39 | }; 40 | 41 | export const isPassword = (password: string): ValidationResponse => { 42 | if (password.length < 8) { 43 | return { result: false, msg: "Requirement over 8 characters" }; 44 | } else if (!number.test(password)) { 45 | return { result: false, msg: "Requirement number" }; 46 | } else if (!upper_alphabet.test(password)) { 47 | return { result: false, msg: "Requirement upper alphabet" }; 48 | } else if (!lower_alphabet.test(password)) { 49 | return { result: false, msg: "Requirement lower alphabet" }; 50 | } else if (!special_characters.test(password)) { 51 | return { 52 | result: false, 53 | msg: `Requirement lower special_characters(${special_character})` 54 | }; 55 | } 56 | return { result: true, msg: "" }; 57 | }; 58 | -------------------------------------------------------------------------------- /client/src/components/ui/books/BooksView.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 75 | -------------------------------------------------------------------------------- /server/src/config/database/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Sequelize, 3 | FindOptions, 4 | CreateOptions, 5 | UpdateOptions, 6 | DestroyOptions 7 | } from "sequelize"; 8 | require("dotenv").config(); 9 | 10 | import { logger } from "../logger"; 11 | import { initDefaultData } from "./default"; 12 | 13 | const sequelize = new Sequelize( 14 | process.env.DB_DATABASE || "", 15 | process.env.DB_USER || "", 16 | process.env.DB_ROOT_PASSWORD || "", 17 | { 18 | host: process.env.DB_HOST || "127.0.0.1", 19 | port: Number.parseInt(process.env.DB_PORT || "3306", 10), 20 | dialect: "mariadb", 21 | dialectOptions: { 22 | connectTimeout: 1000, 23 | timezone: "Etc/GMT0" 24 | }, 25 | pool: { 26 | max: 10, 27 | min: 1, 28 | acquire: 30000, 29 | idle: 20000 30 | }, 31 | define: { 32 | charset: "utf8", 33 | underscored: true, 34 | timestamps: true, 35 | createdAt: "created_at", 36 | updatedAt: "updated_at", 37 | deletedAt: "deleted_at", 38 | paranoid: true 39 | }, 40 | logging: false, 41 | benchmark: false, 42 | hooks: { 43 | beforeFind: (options: FindOptions) => { 44 | logger.debug(`beforeFind`); 45 | logger.debug(options); 46 | }, 47 | beforeCreate: (instance: any, options: CreateOptions) => { 48 | logger.debug(`beforeCreate`); 49 | logger.debug(instance); 50 | }, 51 | beforeUpdate: (instance: any, options: UpdateOptions) => { 52 | logger.debug(`beforeUpdate`); 53 | logger.debug(instance); 54 | }, 55 | beforeDestroy: (instance: any, options: DestroyOptions) => { 56 | logger.debug(`beforeDestroy`); 57 | logger.debug(instance); 58 | } 59 | } 60 | } 61 | ); 62 | 63 | sequelize 64 | .authenticate() 65 | .then(() => { 66 | console.log("Sequelize Connection has been established successfully."); 67 | }) 68 | .catch(err => { 69 | console.error("Unable to connect to the database:", err); 70 | }); 71 | 72 | sequelize.sync({ force: true }).then(() => { 73 | initDefaultData(); 74 | }); 75 | 76 | export { sequelize }; 77 | 78 | export default sequelize; 79 | -------------------------------------------------------------------------------- /server/src/app/routes/graphql/type/index.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLList, GraphQLObjectType } from "graphql"; 2 | import { 3 | GraphQLBoolean, 4 | GraphQLInt, 5 | GraphQLString 6 | } from "graphql/type/scalars"; 7 | 8 | const AuthorityType = new GraphQLObjectType({ 9 | name: "Authority", 10 | fields: { 11 | id: { type: GraphQLInt }, 12 | name: { type: GraphQLString }, 13 | level: { type: GraphQLInt }, 14 | 15 | active: { type: GraphQLBoolean }, 16 | created_at: { type: GraphQLString }, 17 | deleted_at: { type: GraphQLString }, 18 | updated_at: { type: GraphQLString } 19 | } 20 | }); 21 | 22 | const BookType = new GraphQLObjectType({ 23 | name: "Book", 24 | fields: { 25 | id: { type: GraphQLInt }, 26 | name: { type: GraphQLString }, 27 | author: { type: GraphQLString }, 28 | status: { type: GraphQLBoolean }, 29 | description: { type: GraphQLString }, 30 | 31 | active: { type: GraphQLBoolean }, 32 | created_at: { type: GraphQLString }, 33 | deleted_at: { type: GraphQLString }, 34 | updated_at: { type: GraphQLString } 35 | } 36 | }); 37 | 38 | const UserType = new GraphQLObjectType({ 39 | name: "User", 40 | fields: { 41 | id: { type: GraphQLInt }, 42 | name: { type: GraphQLString }, 43 | email: { type: GraphQLString }, 44 | password: { type: GraphQLString }, 45 | birth: { type: GraphQLString }, 46 | 47 | borrowed_books: { type: new GraphQLList(BookType) }, 48 | 49 | active: { type: GraphQLBoolean }, 50 | created_at: { type: GraphQLString }, 51 | deleted_at: { type: GraphQLString }, 52 | updated_at: { type: GraphQLString } 53 | } 54 | }); 55 | 56 | const DivisionType = new GraphQLObjectType({ 57 | name: "Division", 58 | fields: { 59 | id: { type: GraphQLInt }, 60 | name: { type: GraphQLString }, 61 | description: { type: GraphQLString }, 62 | 63 | employees: { type: new GraphQLList(UserType) }, 64 | 65 | active: { type: GraphQLBoolean }, 66 | created_at: { type: GraphQLString }, 67 | deleted_at: { type: GraphQLString }, 68 | updated_at: { type: GraphQLString } 69 | } 70 | }); 71 | 72 | export { AuthorityType, BookType, DivisionType, UserType }; 73 | -------------------------------------------------------------------------------- /server/src/test/repository/book/BookRepository.test.ts: -------------------------------------------------------------------------------- 1 | import { sandbox, stub } from 'sinon'; 2 | import { BookRepository } from '../../../repository/book/BookRepository'; 3 | 4 | const book_repository = new BookRepository(); 5 | describe('#BookRepository', () => { 6 | let sandbox; 7 | let book_repository; 8 | 9 | beforeAll(async () => { 10 | sandbox = sandbox.create(); 11 | book_repository = sandbox.stub(book_repository, 'book_repository'); 12 | }); 13 | 14 | afterAll(async () => { 15 | sandbox.restore(); 16 | }); 17 | 18 | it('Book create', () => { 19 | const create = stub(book_repository, 'create'); 20 | expect(create({name: ''}).then((user) => { 21 | console.log(user); 22 | })).toEqual({name: 'What is the Web'}); 23 | }); 24 | 25 | it('Book findOne by UK', () => { 26 | const findOne = stub(book_repository, 'findOne'); 27 | expect(findOne({name: 'Seolhun'}).then((user) => { 28 | console.log(user); 29 | })).toEqual({name: 'What is the Web'}); 30 | }); 31 | 32 | it('Book findAll', () => { 33 | const findAll = stub(book_repository, 'findAll'); 34 | expect(findAll().then((user) => { 35 | console.log(user); 36 | })).toEqual({name: 'What is the Web'}); 37 | }); 38 | 39 | it('Book findAllByIds', () => { 40 | const findAllByIds = stub(book_repository, 'findAllByIds'); 41 | expect(findAllByIds([1, 2, 3]).then((user) => { 42 | console.log(user); 43 | })).toEqual({name: 'What is the Web'}); 44 | }); 45 | 46 | it('Book findAllByPaging', () => { 47 | const findAllByPaging = stub(book_repository, 'findAllByPaging'); 48 | expect(findAllByPaging({name: 'What is the Web'}, 0, 20).then((user) => { 49 | console.log(user); 50 | })).toEqual({name: 'What is the Web'}); 51 | }); 52 | 53 | it('Book update', () => { 54 | const update = stub(book_repository, 'update'); 55 | expect(update({name: ''}).then((user) => { 56 | console.log(user); 57 | })).toEqual({name: 'What is the Web'}); 58 | }); 59 | 60 | it('Book delete', () => { 61 | const deleteBook = stub(book_repository, 'delete'); 62 | expect(deleteBook({name: ''}).then((user) => { 63 | console.log(user); 64 | })).toEqual({name: 'What is the Web'}); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /server/src/test/repository/division/DivisionRepository.test.ts: -------------------------------------------------------------------------------- 1 | import { sandbox, stub } from 'sinon'; 2 | import { DivisionRepository } from '../../../repository/division/DivisionRepository'; 3 | 4 | const division_repository = new DivisionRepository(); 5 | describe('#DivisionRepository', () => { 6 | let sandbox; 7 | let division_repository; 8 | 9 | beforeAll(async () => { 10 | sandbox = sandbox.create(); 11 | division_repository = sandbox.stub(division_repository, 'division_repository'); 12 | }); 13 | 14 | afterAll(async () => { 15 | sandbox.restore(); 16 | }); 17 | 18 | test('User create', () => { 19 | const create = stub(division_repository, 'create'); 20 | expect(create({name: 'Dev'}).then((user) => { 21 | console.log(user); 22 | })).toEqual({name: 'Dev'}); 23 | }); 24 | 25 | test('User findOne by UK', () => { 26 | const findOne = stub(division_repository, 'findOne'); 27 | expect(findOne({name: 'Dev'}).then((user) => { 28 | console.log(user); 29 | })).toEqual({name: 'Dev'}); 30 | }); 31 | 32 | test('User findAll', () => { 33 | const findAll = stub(division_repository, 'findAll'); 34 | expect(findAll().then((user) => { 35 | console.log(user); 36 | })).toEqual({name: 'Dev'}); 37 | }); 38 | 39 | test('User findAllByIds', () => { 40 | const findAllByIds = stub(division_repository, 'findAllByIds'); 41 | expect(findAllByIds([1, 2, 3]).then((user) => { 42 | console.log(user); 43 | })).toEqual({name: 'Dev'}); 44 | }); 45 | 46 | test('User findAllByPaging', () => { 47 | const findAllByPaging = stub(division_repository, 'findAllByPaging'); 48 | expect(findAllByPaging({name: 'Dev'}, 0, 20).then((user) => { 49 | console.log(user); 50 | })).toEqual({name: 'Dev'}); 51 | }); 52 | 53 | test('User update', () => { 54 | const update = stub(division_repository, 'update'); 55 | expect(update({name: 'Dev'}).then((user) => { 56 | console.log(user); 57 | })).toEqual({name: 'Dev'}); 58 | }); 59 | 60 | test('User delete', () => { 61 | const deleteDivision = stub(division_repository, 'delete'); 62 | expect(deleteDivision({name: 'Dev'}).then((user) => { 63 | console.log(user); 64 | })).toEqual({name: 'Dev'}); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /server/src/app/services/DivisionService.ts: -------------------------------------------------------------------------------- 1 | import { Division } from "../types"; 2 | import { DivisionRepository } from "./repository"; 3 | import { Order } from "./repository/AbstractRepository"; 4 | import { DivisionModel } from "./model"; 5 | 6 | const divisionRepository = new DivisionRepository(["id", "name"]); 7 | class DivisionService { 8 | createdDivision = async ({ 9 | name, 10 | description 11 | }: Division): Promise => { 12 | if (!name && !description) { 13 | return new Error("The name and description are requirement."); 14 | } 15 | 16 | const division = await divisionRepository.findOne({ name }); 17 | if (!division) { 18 | return await divisionRepository.create({ name, description }); 19 | } 20 | 21 | if (division.get("name") === name) { 22 | return Promise.reject(new Error(`Already '${name}' is exists.`)); 23 | } 24 | return Promise.reject(new Error(`Already '${name}' is exists.`)); 25 | }; 26 | 27 | findOne({ id, name }: Division): Promise { 28 | if (!id && !name) { 29 | return Promise.reject(new Error("One of id and name are requirement.")); 30 | } 31 | return divisionRepository.findOne({ id, name }); 32 | } 33 | 34 | findAll(order: Order): Promise { 35 | return divisionRepository.findAll(order); 36 | } 37 | 38 | updatedDivision({ id, name, description }: Division): Promise { 39 | if (!id && !name) { 40 | return Promise.reject(new Error("One of id and name are requirement.")); 41 | } 42 | 43 | return divisionRepository.findOne({ id, name }).then(db_division => { 44 | if (!db_division) { 45 | return Promise.reject(new Error("The division is not found")); 46 | } 47 | return divisionRepository.update({ name, description }); 48 | }); 49 | } 50 | 51 | deletedDivision({ id, name }: Division): Promise { 52 | if (!id && !name) { 53 | return Promise.reject(new Error("One of id and name is requirement.")); 54 | } 55 | const db_division = divisionRepository.findOne({ id, name }); 56 | if (!db_division) { 57 | return Promise.reject(new Error("The division is not found")); 58 | } 59 | return divisionRepository.delete({ id }); 60 | } 61 | } 62 | 63 | export { DivisionService }; 64 | -------------------------------------------------------------------------------- /server/src/app/services/repository/UserRepository.ts: -------------------------------------------------------------------------------- 1 | import Sequelize from "sequelize"; 2 | 3 | import { User } from "../../types"; 4 | import { UserModel } from "../model"; 5 | import { AbstractRepository, Order } from "./AbstractRepository"; 6 | 7 | class UserRepository extends AbstractRepository { 8 | // ['id', 'email', 'name']); 9 | unique_criterias: string[] = []; 10 | 11 | constructor(unique_criterias) { 12 | super(); 13 | this.unique_criterias = unique_criterias; 14 | } 15 | 16 | create = async (user: User) => { 17 | return UserModel.create(user); 18 | }; 19 | 20 | findOne = async (user: User) => { 21 | const params = this.getUniqueCriteria(user, this.unique_criterias); 22 | return UserModel.findOne({ 23 | where: { 24 | [Sequelize.Op.or]: params 25 | } 26 | }); 27 | }; 28 | 29 | findAll = async (order?: Order) => { 30 | if (!order) { 31 | order = "DESC"; 32 | } 33 | return UserModel.findAll({ 34 | order: [["created_at", order]] 35 | }); 36 | }; 37 | 38 | findAllByPaging = async ( 39 | user: User, 40 | offset?: number | 0, 41 | limit?: number | 20, 42 | order?: Order 43 | ) => { 44 | if (!order) { 45 | order = "DESC"; 46 | } 47 | return UserModel.findAll({ 48 | offset, 49 | limit, 50 | // where: user, 51 | order: [["created_at", order]] 52 | }); 53 | }; 54 | 55 | findAllByIds = async (ids: number[], order?: Order) => { 56 | if (!order) { 57 | order = "DESC"; 58 | } 59 | return UserModel.findAll({ 60 | where: { 61 | id: [...ids] 62 | }, 63 | order: [["created_at", order]] 64 | }); 65 | }; 66 | 67 | update = async (user: User) => { 68 | const params = this.getUniqueCriteria(user, this.unique_criterias); 69 | try { 70 | UserModel.update(user, { 71 | where: { 72 | [Sequelize.Op.or]: params 73 | } 74 | }); 75 | } catch (error) { 76 | return false; 77 | } 78 | return true; 79 | }; 80 | 81 | delete = async (user: User) => { 82 | const params = this.getUniqueCriteria(user, this.unique_criterias); 83 | try { 84 | UserModel.destroy({ 85 | where: { 86 | [Sequelize.Op.or]: params 87 | } 88 | }); 89 | } catch (error) { 90 | return false; 91 | } 92 | return true; 93 | }; 94 | } 95 | 96 | export { UserRepository }; 97 | -------------------------------------------------------------------------------- /server/src/test/repository/user/UserRepository.test.ts: -------------------------------------------------------------------------------- 1 | import { sandbox, stub } from 'sinon'; 2 | import { UserRepository } from '../../../repository/user/UserRepository'; 3 | 4 | const user_repository = new UserRepository(); 5 | describe('#UserRepository', () => { 6 | let sandbox; 7 | 8 | beforeAll(async () => { 9 | sandbox = sandbox.create(); 10 | }); 11 | 12 | afterAll(async () => { 13 | sandbox.restore(); 14 | }); 15 | 16 | test('User create', () => { 17 | const create = stub(user_repository, 'create'); 18 | expect(create({email: 'shun10114@gmail.com', name: ''}).then((user) => { 19 | console.log(user); 20 | })).toEqual({email: 'shun10114@gmail.com', name: 'Seolhun'}); 21 | }); 22 | 23 | test('User findOne by UK', () => { 24 | const findOne = stub(user_repository, 'findOne'); 25 | expect(findOne({email: 'shun10114@gmail.com', name: ''}).then((user) => { 26 | console.log(user); 27 | })).toEqual({email: 'shun10114@gmail.com', name: 'Seolhun'}); 28 | }); 29 | 30 | test('User findAll', () => { 31 | const findAll = stub(user_repository, 'findAll'); 32 | expect(findAll().then((user) => { 33 | console.log(user); 34 | })).toEqual({email: 'shun10114@gmail.com', name: 'Seolhun'}); 35 | }); 36 | 37 | test('User findAllByIds', () => { 38 | const findAllByIds = stub(user_repository, 'findAllByIds'); 39 | expect(findAllByIds([1, 2, 3]).then((user) => { 40 | console.log(user); 41 | })).toEqual({email: 'shun10114@gmail.com', name: 'Seolhun'}); 42 | }); 43 | 44 | test('User findAllByPaging', () => { 45 | const findAllByPaging = stub(user_repository, 'findAllByPaging'); 46 | expect(findAllByPaging({email: 'shun10114@gmail.com', name: ''}, 0, 20).then((user) => { 47 | console.log(user); 48 | })).toEqual({email: 'shun10114@gmail.com', name: 'Seolhun'}); 49 | }); 50 | 51 | test('User update', () => { 52 | const update = stub(user_repository, 'update'); 53 | expect(update({email: 'shun10114@gmail.com', name: ''}).then((user) => { 54 | console.log(user); 55 | })).toEqual({email: 'shun10114@gmail.com', name: 'Seolhun'}); 56 | }); 57 | 58 | test('User delete', () => { 59 | const deleteUser = stub(user_repository, 'delete'); 60 | expect(deleteUser({email: 'shun10114@gmail.com', name: ''}).then((user) => { 61 | console.log(user); 62 | })).toEqual({email: 'shun10114@gmail.com', name: 'Seolhun'}); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /server/src/app/services/repository/BookRepository.ts: -------------------------------------------------------------------------------- 1 | import { Op } from "sequelize"; 2 | 3 | import { Book } from "../../types"; 4 | import { BookModel } from "../model"; 5 | import { AbstractRepository, Order } from "./AbstractRepository"; 6 | 7 | class BookRepository extends AbstractRepository { 8 | // ['id'] 9 | unique_criterias: string[] = []; 10 | 11 | constructor(unique_criterias) { 12 | super(); 13 | this.unique_criterias = unique_criterias; 14 | } 15 | 16 | create = async (book: Book) => { 17 | return BookModel.create(book); 18 | }; 19 | 20 | findOne = async (book: Book) => { 21 | const params = this.getUniqueCriteria(book, this.unique_criterias); 22 | return BookModel.findOne({ 23 | where: { 24 | [Op.or]: params 25 | } 26 | }); 27 | }; 28 | 29 | findAll = async (order?: Order) => { 30 | if (!order) { 31 | order = "DESC"; 32 | } 33 | return BookModel.findAll({ 34 | order: [["created_at", order]] 35 | }); 36 | }; 37 | 38 | findAllByPaging = async ( 39 | book: Book, 40 | offset?: number | 0, 41 | limit?: number | 20, 42 | order?: Order 43 | ) => { 44 | if (!order) { 45 | order = "DESC"; 46 | } 47 | return BookModel.findAll({ 48 | offset, 49 | limit, 50 | // where: book, 51 | order: [["created_at", order]] 52 | }); 53 | }; 54 | 55 | findAllByIds = async (ids: number[], order?: Order) => { 56 | if (!order) { 57 | order = "DESC"; 58 | } 59 | const db_books = BookModel.findAll({ 60 | where: { 61 | id: { 62 | [Op.or]: [ids] 63 | } 64 | }, 65 | order: [["created_at", order]] 66 | }); 67 | return db_books; 68 | }; 69 | 70 | update = async (book: Book) => { 71 | const params = this.getUniqueCriteria(book, this.unique_criterias); 72 | try { 73 | BookModel.update(book, { 74 | where: { 75 | [Op.or]: params 76 | } 77 | }); 78 | } catch (error) { 79 | return false; 80 | } 81 | return true; 82 | }; 83 | 84 | delete = async (book: Book) => { 85 | const params = this.getUniqueCriteria(book, this.unique_criterias); 86 | try { 87 | BookModel.destroy({ 88 | where: { 89 | [Op.or]: params 90 | } 91 | }); 92 | } catch (error) { 93 | return false; 94 | } 95 | return true; 96 | }; 97 | } 98 | 99 | export { BookRepository }; 100 | -------------------------------------------------------------------------------- /server/src/app/services/repository/DivisionRepository.ts: -------------------------------------------------------------------------------- 1 | import Sequelize from "sequelize"; 2 | 3 | import { Division } from "../../types"; 4 | import { DivisionModel } from "../model"; 5 | import { AbstractRepository, Order } from "./AbstractRepository"; 6 | 7 | class DivisionRepository extends AbstractRepository { 8 | // ['id', 'name'] 9 | unique_criterias: string[] = []; 10 | 11 | constructor(unique_criterias) { 12 | super(); 13 | this.unique_criterias = unique_criterias; 14 | } 15 | 16 | create = async (division: Division) => { 17 | const db_division = DivisionModel.create(division); 18 | return db_division; 19 | }; 20 | 21 | findOne = async (division: Division) => { 22 | const params = this.getUniqueCriteria(division, this.unique_criterias); 23 | return DivisionModel.findOne({ 24 | where: { 25 | [Sequelize.Op.or]: params 26 | } 27 | }); 28 | }; 29 | 30 | findAll = async (order?: Order) => { 31 | if (!order) { 32 | order = "DESC"; 33 | } 34 | return DivisionModel.findAll({ 35 | order: [["created_at", order]] 36 | }); 37 | }; 38 | 39 | findAllByPaging = async ( 40 | divisions: Division, 41 | offset?: number | 0, 42 | limit?: number | 20, 43 | order?: Order 44 | ) => { 45 | if (!order) { 46 | order = "DESC"; 47 | } 48 | return DivisionModel.findAll({ 49 | offset, 50 | limit, 51 | // where: divisions, 52 | order: [["created_at", order]] 53 | }); 54 | }; 55 | 56 | findAllByIds = async (ids: number[], order?: Order) => { 57 | if (!order) { 58 | order = "DESC"; 59 | } 60 | return DivisionModel.findAll({ 61 | where: { 62 | id: [...ids] 63 | }, 64 | order: [["created_at", order]] 65 | }); 66 | }; 67 | 68 | update = async (division: Division) => { 69 | const params = this.getUniqueCriteria(division, this.unique_criterias); 70 | try { 71 | DivisionModel.update(division, { 72 | where: { 73 | [Sequelize.Op.or]: params 74 | } 75 | }); 76 | } catch (error) { 77 | return false; 78 | } 79 | return true; 80 | }; 81 | 82 | delete = async (division: Division) => { 83 | const params = this.getUniqueCriteria(division, this.unique_criterias); 84 | try { 85 | DivisionModel.destroy({ 86 | where: { 87 | [Sequelize.Op.or]: params 88 | } 89 | }); 90 | } catch (error) { 91 | return false; 92 | } 93 | return true; 94 | }; 95 | } 96 | 97 | export { DivisionRepository }; 98 | -------------------------------------------------------------------------------- /server/src/app.ts: -------------------------------------------------------------------------------- 1 | import bodyParser from "body-parser"; 2 | import cookieSession from "cookie-session"; 3 | import cors from "cors"; 4 | import express from "express"; 5 | import graphqlHTTP from "express-graphql"; 6 | import session from "express-session"; 7 | import helmet from "helmet"; 8 | 9 | import { passport } from "./config/auth/passport"; 10 | import { schema } from "./app/routes/graphql/schema"; 11 | import { Config } from "./config/environments"; 12 | 13 | import { auth_router, user_router } from "./app/routes/auth"; 14 | 15 | const env = Config.setConfiguration(); 16 | const app = express(); 17 | 18 | // Middleware 19 | app.use(bodyParser.json()); 20 | app.use(cors()); 21 | app.use(helmet()); 22 | app.disable("x-powered-by"); 23 | 24 | // Session 25 | const expiryDate = new Date(Date.now() + 1000 * 60 * 30); // 30 min 26 | app.use( 27 | cookieSession({ 28 | name: "cookie-session-id", 29 | maxAge: env.COOKIE_SESSION_MAX_AGE, 30 | keys: env.COOKIE_SESSION_KEYS 31 | }) 32 | ); 33 | app.use( 34 | session({ 35 | name: "session-id", 36 | secret: "hunseol_typescript_graphql", 37 | resave: false, 38 | saveUninitialized: true, 39 | maxAge: env.SESSION_MAX_AGE, 40 | keys: env.SESSION_KEYS, 41 | cookie: { 42 | secure: true, 43 | httpOnly: true, 44 | expires: expiryDate 45 | } 46 | }) 47 | ); 48 | app.set("trust proxy", 1); 49 | 50 | // PasportJS 51 | app.use(passport.initialize()); 52 | app.use(passport.session()); 53 | 54 | // GraphQL 55 | app.use( 56 | "/graphql", 57 | graphqlHTTP(async request => { 58 | const startTime = Date.now(); 59 | return { 60 | schema, 61 | graphiql: process.env.NODE_ENV !== "production", 62 | extensions({ document, variables, operationName, result }) { 63 | return { 64 | result: bodyParser.json(result), 65 | variables, 66 | operationName, 67 | runTime: Date.now() - startTime 68 | }; 69 | } 70 | }; 71 | }) 72 | ); 73 | 74 | // Express Router 75 | app.get("/", (req, res) => { 76 | res.send("Hello World!!, BookManagement System by Seolhun"); 77 | }); 78 | 79 | // Sub Routes 80 | app.use("/auth", auth_router); 81 | app.use("/user", user_router); 82 | 83 | // Run Server 84 | app 85 | .listen(env.EXPRESS_PORT, () => { 86 | console.log("=========================app.ts==========================="); 87 | console.log(`Listening the server ${env.EXPRESS_PORT}`); 88 | console.log("===================================================="); 89 | }) 90 | .on("error", err => { 91 | console.error(err); 92 | }); 93 | -------------------------------------------------------------------------------- /client/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Router, { Route, RouteConfig } from "vue-router"; 3 | 4 | import HomeView from "@/components/HomeView.vue"; 5 | import LoginView from "@/components/ui/auth/LoginView.vue"; 6 | import SigninView from "@/components/ui/auth/SigninView.vue"; 7 | 8 | import BookDetailView from "@/components/ui/books/BookDetailView.vue"; 9 | import BooksView from "@/components/ui/books/BooksView.vue"; 10 | import DivisionDetailView from "@/components/ui/divisions/DivisionDetailView.vue"; 11 | import DivisionsView from "@/components/ui/divisions/DivisionsView.vue"; 12 | import UserDetailView from "@/components/ui/users/UserDetailView.vue"; 13 | import UsersView from "@/components/ui/users/UsersView.vue"; 14 | 15 | import { isAuthorized } from "../utils/Authorize"; 16 | 17 | Vue.use(Router); 18 | const routes: RouteConfig[] = [ 19 | { 20 | path: "/", 21 | name: "Home", 22 | component: HomeView 23 | }, 24 | { 25 | path: "/books", 26 | name: "Books", 27 | component: BooksView 28 | // meta: { requiresAuth: true }, 29 | }, 30 | { 31 | path: "/books/:id", 32 | name: "BookDetail", 33 | component: BookDetailView 34 | }, 35 | { 36 | path: "/divisions", 37 | name: "Divisions", 38 | component: DivisionsView 39 | }, 40 | { 41 | path: "/divisions/:name", 42 | name: "DivisionDetail", 43 | component: DivisionDetailView 44 | }, 45 | { 46 | path: "/login", 47 | name: "Login", 48 | component: LoginView, 49 | meta: { requiresAuth: false } 50 | }, 51 | { 52 | path: "/signin", 53 | name: "Signin", 54 | component: SigninView, 55 | meta: { requiresAuth: false } 56 | }, 57 | { 58 | path: "/users", 59 | name: "Users", 60 | component: UsersView 61 | // meta: { requiresAuth: true }, 62 | // redirect: '/home', 63 | // meta: { leaf: false, icon: 'icon-article' }, 64 | }, 65 | { 66 | path: "/users/:name", 67 | name: "UserDetail", 68 | component: UserDetailView 69 | } 70 | ]; 71 | 72 | const router: Router = new Router({ 73 | mode: "history", 74 | routes 75 | }); 76 | 77 | router.beforeEach( 78 | (to: Route, from: Route, next: (prams?: any) => void): void => { 79 | if (!from) { 80 | next(); 81 | } 82 | if (to.matched.some(record => record.meta.requiresAuth)) { 83 | if (!isAuthorized()) { 84 | next({ 85 | path: "/login", 86 | query: { redirect: to.fullPath } 87 | }); 88 | } else { 89 | next(); 90 | } 91 | } else { 92 | next(); 93 | } 94 | } 95 | ); 96 | 97 | export default router; 98 | -------------------------------------------------------------------------------- /server/src/app/routes/graphql/mutation/UserMutation.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLFieldConfigMap, GraphQLNonNull } from "graphql"; 2 | import { GraphQLInt, GraphQLString } from "graphql/type/scalars"; 3 | import { UserType } from "../type/index"; 4 | 5 | import { DivisionService, UserService } from "../../../services"; 6 | import { User } from "../../../types"; 7 | 8 | const division_service = new DivisionService(); 9 | const user_service = new UserService(); 10 | 11 | const UserMutation: GraphQLFieldConfigMap = { 12 | // Basic 13 | addUser: { 14 | type: UserType, 15 | args: { 16 | email: { type: new GraphQLNonNull(GraphQLString) }, 17 | name: { type: new GraphQLNonNull(GraphQLString) }, 18 | password: { type: new GraphQLNonNull(GraphQLString) }, 19 | birth: { type: new GraphQLNonNull(GraphQLString) }, 20 | division_id: { type: new GraphQLNonNull(GraphQLInt) } 21 | }, 22 | async resolve( 23 | parent, 24 | { email, password, birth, name, division_id }: User, 25 | context, 26 | info 27 | ) { 28 | return await user_service.createdUser({ 29 | email, 30 | birth, 31 | name, 32 | password, 33 | division_id 34 | }); 35 | } 36 | }, 37 | editUser: { 38 | type: UserType, 39 | args: { 40 | id: { type: GraphQLInt }, 41 | email: { type: GraphQLString }, 42 | name: { type: GraphQLString }, 43 | password: { type: new GraphQLNonNull(GraphQLString) }, 44 | birth: { type: GraphQLString }, 45 | division: { type: GraphQLInt } 46 | }, 47 | async resolve( 48 | parent, 49 | { id, email, password, birth, name, division_id }: User 50 | ) { 51 | return await user_service.updatedUser({ 52 | id, 53 | email, 54 | password, 55 | birth, 56 | name, 57 | division_id 58 | }); 59 | } 60 | }, 61 | deleteUser: { 62 | type: UserType, 63 | args: { 64 | id: { type: GraphQLInt }, 65 | email: { type: GraphQLString } 66 | }, 67 | async resolve(parent, { id, email, password }: User) { 68 | return await user_service.deletedUser({ id, email, name, password }); 69 | } 70 | }, 71 | 72 | // Custom 73 | loginUser: { 74 | type: UserType, 75 | args: { 76 | email: { type: GraphQLString }, 77 | name: { type: GraphQLString }, 78 | password: { type: new GraphQLNonNull(GraphQLString) } 79 | }, 80 | async resolve(parent, { email, name, password }: User) { 81 | return await user_service.loginUser({ email, name, password }); 82 | } 83 | } 84 | }; 85 | 86 | export { UserMutation }; 87 | -------------------------------------------------------------------------------- /client/src/models/index.ts: -------------------------------------------------------------------------------- 1 | class AuthorityModel { 2 | id?: number; 3 | name?: string; 4 | level?: number; 5 | description?: string; 6 | 7 | active?: boolean; 8 | created_at?: string; 9 | updated_at?: string; 10 | deleted_at?: string; 11 | 12 | constructor() { 13 | this.id = 0; 14 | this.name = ""; 15 | this.level = 0; 16 | this.description = ""; 17 | 18 | this.active = true; 19 | this.created_at = ""; 20 | this.deleted_at = ""; 21 | this.updated_at = ""; 22 | } 23 | } 24 | 25 | class BookModel { 26 | id?: number; 27 | name?: string; 28 | author?: string; 29 | description?: string; 30 | status?: boolean; 31 | 32 | active?: boolean; 33 | created_at?: string; 34 | deleted_at?: string; 35 | updated_at?: string; 36 | 37 | constructor() { 38 | this.id = 0; 39 | this.name = ""; 40 | this.author = ""; 41 | this.description = ""; 42 | this.status = true; 43 | 44 | this.active = true; 45 | this.created_at = ""; 46 | this.deleted_at = ""; 47 | this.updated_at = ""; 48 | } 49 | } 50 | 51 | class DivisionModel { 52 | id?: number; 53 | name?: string; 54 | description?: string; 55 | 56 | active?: boolean; 57 | created_at?: string; 58 | deleted_at?: string; 59 | updated_at?: string; 60 | 61 | constructor() { 62 | this.id = 0; 63 | this.name = ""; 64 | this.description = ""; 65 | 66 | this.active = true; 67 | this.created_at = ""; 68 | this.deleted_at = ""; 69 | this.updated_at = ""; 70 | } 71 | } 72 | 73 | class UserModel { 74 | id: number; 75 | name: string; 76 | email: string; 77 | password: string; 78 | confirm_password: string; 79 | 80 | birth?: string; 81 | division_id?: number; 82 | division?: DivisionModel; 83 | 84 | active?: boolean; 85 | created_at?: string; 86 | deleted_at?: string; 87 | updated_at?: string; 88 | 89 | validation?: { 90 | email: string; 91 | name: string; 92 | password: string; 93 | confirm_password: string; 94 | }; 95 | is_active?: boolean; 96 | is_submit?: boolean; 97 | 98 | constructor() { 99 | this.id = 0; 100 | this.name = ""; 101 | this.email = ""; 102 | this.password = ""; 103 | this.confirm_password = ""; 104 | 105 | this.birth = ""; 106 | this.division_id = 0; 107 | this.division = new DivisionModel(); 108 | 109 | this.active = true; 110 | this.created_at = ""; 111 | this.deleted_at = ""; 112 | this.updated_at = ""; 113 | } 114 | } 115 | 116 | export { AuthorityModel, BookModel, DivisionModel, UserModel }; 117 | -------------------------------------------------------------------------------- /client/src/components/layout/FooterMenu.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 79 | 80 | 83 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@seolhun/vue-type-graphql-example", 3 | "author": "SeolHun", 4 | "version": "0.1.4", 5 | "description": "TypeScript Mastering Project", 6 | "main": "main.ts", 7 | "license": "MIT", 8 | "private": true, 9 | "scripts": { 10 | "dev": "vue-cli-service serve", 11 | "build": "vue-cli-service build", 12 | "lint": "vue-cli-service lint", 13 | "test:unit": "vue-cli-service test:unit" 14 | }, 15 | "dependencies": { 16 | "apollo-cache-inmemory": "^1.1.5", 17 | "apollo-client": "^2.2.0", 18 | "apollo-link": "^1.0.7", 19 | "apollo-link-http": "^1.5.15", 20 | "axios": "^0.19.0", 21 | "bootstrap-vue": "^2.0.0-rc.0", 22 | "classnames": "^2.2.6", 23 | "core-js": "^3.3.0", 24 | "graphql": "^14.2.0", 25 | "graphql-tag": "^2.6.1", 26 | "lodash": "^4.17.4", 27 | "register-service-worker": "^1.6.2", 28 | "validator": "^12.0.0", 29 | "vue": "^2.6.10", 30 | "vue-apollo": "^3.0.0-rc.2", 31 | "vue-class-component": "^7.1.0", 32 | "vue-i18n": "^8.14.0", 33 | "vue-property-decorator": "^8.2.2", 34 | "vue-router": "^3.1.3", 35 | "vuex": "^3.0.1" 36 | }, 37 | "devDependencies": { 38 | "@types/axios": "^0.14.0", 39 | "@types/jest": "^24.0.11", 40 | "@types/lodash": "^4.14.148", 41 | "@types/typescript": "^2.0.0", 42 | "@types/validator": "^12.0.0", 43 | "@types/vue": "^2.0.0", 44 | "@types/vue-i18n": "^7.0.0", 45 | "@vue/cli-plugin-babel": "^4.1.1", 46 | "@vue/cli-plugin-eslint": "^4.1.1", 47 | "@vue/cli-plugin-pwa": "^4.0.0", 48 | "@vue/cli-plugin-typescript": "^4.0.0", 49 | "@vue/cli-plugin-unit-jest": "^4.1.1", 50 | "@vue/cli-service": "^4.0.0", 51 | "@vue/eslint-config-prettier": "^6.0.0", 52 | "@vue/eslint-config-typescript": "^5.0.1", 53 | "@vue/test-utils": "1.0.0-beta.30", 54 | "babel-core": "7.0.0-bridge.0", 55 | "babel-eslint": "^10.0.1", 56 | "eslint": "^6.4.0", 57 | "eslint-plugin-prettier": "^3.1.0", 58 | "eslint-plugin-vue": "^6.0.0", 59 | "node-sass": "^4.9.0", 60 | "prettier": "^1.18.2", 61 | "sass-loader": "^7.1.0", 62 | "ts-jest": "^24.1.0", 63 | "typescript": "^3.5.3", 64 | "vue-template-compiler": "^2.6.10" 65 | }, 66 | "engines": { 67 | "node": ">= 4.0.0", 68 | "npm": ">= 3.0.0" 69 | }, 70 | "directories": { 71 | "test": "test" 72 | }, 73 | "repository": { 74 | "type": "git", 75 | "url": "git+https://github.com/Seolhun//vue-type-graphql-example.git" 76 | }, 77 | "keywords": [ 78 | "/vue-type-graphql-example" 79 | ], 80 | "bugs": { 81 | "url": "https://github.com/Seolhun//vue-type-graphql-example/issues" 82 | }, 83 | "homepage": "https://github.com/Seolhun//vue-type-graphql-example#readme" 84 | } 85 | -------------------------------------------------------------------------------- /server/src/app/services/repository/AuthorityRepository.ts: -------------------------------------------------------------------------------- 1 | import Sequelize, { Op } from "sequelize"; 2 | 3 | import { Authority } from "../../types"; 4 | import { AuthorityModel } from "../model"; 5 | import { AbstractRepository, Order } from "./AbstractRepository"; 6 | 7 | class AuthorityRepository extends AbstractRepository { 8 | // ['id', 'name'] 9 | unique_criterias: string[] = []; 10 | 11 | constructor(unique_criterias) { 12 | super(); 13 | this.unique_criterias = unique_criterias; 14 | } 15 | 16 | create = async (user: Authority): Promise => { 17 | return AuthorityModel.create(user); 18 | }; 19 | 20 | findOne = async (user: Authority): Promise => { 21 | const params = this.getUniqueCriteria(user, this.unique_criterias); 22 | return AuthorityModel.findOne({ 23 | where: { 24 | [Sequelize.Op.or]: params 25 | } 26 | }); 27 | }; 28 | 29 | findAll = async (order?: Order): Promise => { 30 | if (!order) { 31 | order = "DESC"; 32 | } 33 | return AuthorityModel.findAll({ 34 | order: [["created_at", order]] 35 | }); 36 | }; 37 | 38 | findAllByPaging = async ( 39 | user: Authority, 40 | offset?: number | 0, 41 | limit?: number | 20, 42 | order?: Order 43 | ) => { 44 | if (!order) { 45 | order = "DESC"; 46 | } 47 | return AuthorityModel.findAll({ 48 | offset, 49 | limit, 50 | where: { 51 | name: "user", 52 | id: { 53 | [Op.or]: [[1, 2, 3]] 54 | } 55 | }, 56 | order: [["created_at", order]] 57 | }); 58 | }; 59 | 60 | findAllByIds = async (ids: number[], order?: Order) => { 61 | if (!order) { 62 | order = "DESC"; 63 | } 64 | 65 | return AuthorityModel.findAll({ 66 | where: { 67 | name: "user", 68 | id: { 69 | [Op.or]: [ids] 70 | } 71 | }, 72 | order: [["created_at", order]] 73 | }); 74 | }; 75 | 76 | update = async (user: Authority) => { 77 | const params = this.getUniqueCriteria(user, this.unique_criterias); 78 | try { 79 | AuthorityModel.update(user, { 80 | where: { 81 | [Sequelize.Op.or]: params 82 | } 83 | }); 84 | } catch (error) { 85 | return false; 86 | } 87 | return true; 88 | }; 89 | 90 | delete = async (user: Authority): Promise => { 91 | const params = this.getUniqueCriteria(user, this.unique_criterias); 92 | try { 93 | AuthorityModel.destroy({ 94 | where: { 95 | [Sequelize.Op.or]: params 96 | } 97 | }); 98 | } catch (error) { 99 | return false; 100 | } 101 | return true; 102 | }; 103 | } 104 | 105 | export { AuthorityRepository }; 106 | -------------------------------------------------------------------------------- /client/src/components/ui/auth/LoginView.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 98 | -------------------------------------------------------------------------------- /server/src/config/database/default/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthorityModel, 3 | BookModel, 4 | DivisionModel, 5 | UserAuthorityModel, 6 | UserModel, 7 | } from "../../../app/services/model"; 8 | import { 9 | Authority, 10 | Book, 11 | BookStatus, 12 | Division, 13 | User, 14 | UserAuthority, 15 | } from "../../../app/types"; 16 | 17 | import { PasswordEncoderUtils } from "../../../app/utils/PasswordEncoderUtils"; 18 | 19 | const authority_datas: Authority[] = [ 20 | { 21 | name: "Admin", 22 | level: 0, 23 | }, 24 | { 25 | name: "Back", 26 | level: 1, 27 | }, 28 | { 29 | name: "Front", 30 | level: 2, 31 | }, 32 | { 33 | name: "Data", 34 | level: 3, 35 | }, 36 | { 37 | name: "Test", 38 | level: 4, 39 | }, 40 | { 41 | name: "Alliance", 42 | level: 5, 43 | }, 44 | { 45 | name: "Etc", 46 | level: 6, 47 | }, 48 | { 49 | name: "Guest", 50 | level: 7, 51 | }, 52 | ]; 53 | 54 | const book_datas: Book[] = [ 55 | { 56 | name: "동시성의 원리", 57 | author: "폴 부쳐", 58 | status: BookStatus.NORMAL, 59 | description: "소프트웨어의 한계를 돌파하는 동시성의 원리", 60 | }, 61 | { 62 | name: "ORM Frameowork", 63 | author: "Orm", 64 | status: BookStatus.NORMAL, 65 | description: "SQL 추상화를 위한 객체지향의 방법", 66 | }, 67 | { 68 | name: "처음 시작하는 파이썬", 69 | author: "Python", 70 | status: BookStatus.ORDERED, 71 | description: "Getting started python", 72 | }, 73 | { 74 | name: "Hadoop is basic", 75 | author: "Hadoop", 76 | status: BookStatus.BORROWED, 77 | description: "빅데이터를 위한 하둡", 78 | }, 79 | { 80 | name: "You don't know JS", 81 | author: "JS", 82 | status: BookStatus.REQUESTED, 83 | description: "JS 기본을 파헤치는 책", 84 | }, 85 | ]; 86 | 87 | const division_datas: Division[] = [ 88 | { 89 | name: "Dev", 90 | description: "Development", 91 | }, 92 | { 93 | name: "Finance", 94 | description: "Finance", 95 | }, 96 | { 97 | name: "HR", 98 | description: "Human Resources", 99 | }, 100 | { 101 | name: "Marketing", 102 | description: "Marketing", 103 | }, 104 | { 105 | name: "Alliance", 106 | description: "Alliance", 107 | }, 108 | ]; 109 | 110 | const user_datas: User[] = [ 111 | { 112 | name: "Shun", 113 | email: "shun@google.com", 114 | birth: "19900126", 115 | division_id: 1, 116 | password: PasswordEncoderUtils.bcryptedPasswordSync("1234"), 117 | }, 118 | { 119 | name: "Mark", 120 | email: "mark@google.com", 121 | birth: "19940606", 122 | division_id: 2, 123 | password: PasswordEncoderUtils.bcryptedPasswordSync("1234"), 124 | }, 125 | { 126 | name: "Gabriel", 127 | email: "gabriel@naver.com", 128 | birth: "19881212", 129 | division_id: 3, 130 | password: PasswordEncoderUtils.bcryptedPasswordSync("1234"), 131 | }, 132 | { 133 | name: "Chris", 134 | email: "chris@hanmail.net", 135 | birth: "19800322", 136 | division_id: 4, 137 | password: PasswordEncoderUtils.bcryptedPasswordSync("1234"), 138 | }, 139 | ]; 140 | 141 | const authority_user_datas: UserAuthority[] = [ 142 | { 143 | user_id: 1, 144 | authority_id: 1, 145 | }, 146 | { 147 | user_id: 2, 148 | authority_id: 6, 149 | }, 150 | { 151 | user_id: 3, 152 | authority_id: 8, 153 | }, 154 | { 155 | user_id: 4, 156 | authority_id: 8, 157 | }, 158 | ]; 159 | 160 | function initDefaultData() { 161 | DivisionModel.bulkCreate(division_datas); 162 | AuthorityModel.bulkCreate(authority_datas).then(() => { 163 | UserModel.bulkCreate(user_datas).then(users => { 164 | UserAuthorityModel.bulkCreate(authority_user_datas); 165 | }); 166 | }); 167 | BookModel.bulkCreate(book_datas); 168 | } 169 | 170 | export { initDefaultData }; 171 | -------------------------------------------------------------------------------- /client/src/components/layout/HeaderMenu.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 112 | 113 | 116 | -------------------------------------------------------------------------------- /server/src/config/auth/passport.ts: -------------------------------------------------------------------------------- 1 | import passport from "passport"; 2 | import facebook from "passport-facebook"; 3 | import github from "passport-github2"; 4 | import google from "passport-google-oauth20"; 5 | 6 | import DEV_CONFIG from "../environments/develop"; 7 | 8 | import { UserService } from "../../app/services"; 9 | import { User } from "../../app/types"; 10 | 11 | const user_service = new UserService(); 12 | 13 | passport.serializeUser((user: User, done) => { 14 | done(null, user.id); 15 | }); 16 | 17 | passport.deserializeUser((id: number, done) => { 18 | user_service.findOne({ id }).then(user => { 19 | if (user) { 20 | done(null, user); 21 | } 22 | }); 23 | }); 24 | 25 | // Github 26 | passport.use( 27 | new github.Strategy( 28 | { 29 | clientID: DEV_CONFIG.GITHUB_CLIENT_ID, 30 | clientSecret: DEV_CONFIG.GITHUB_CLIENT_SECRET_ID, 31 | callbackURL: "/auth/github/callback" 32 | }, 33 | (accessToken, refreshToken, profile, done) => { 34 | console.log("=======Github profile========"); 35 | console.log(profile); 36 | console.log("==============="); 37 | user_service 38 | .findOne({ 39 | id: profile.id, 40 | name: profile.username, 41 | github_id: profile.id 42 | }) 43 | .then(existing_user => { 44 | if (existing_user) { 45 | done(null, existing_user); 46 | } else { 47 | user_service 48 | .createdUserWithOauth2({ 49 | email: profile.email, 50 | name: profile.username, 51 | github_id: profile.id 52 | }) 53 | .then(user => { 54 | done(null, user); 55 | }); 56 | } 57 | }); 58 | } 59 | ) 60 | ); 61 | 62 | // Google 63 | passport.use( 64 | new google.Strategy( 65 | { 66 | clientID: DEV_CONFIG.GITHUB_CLIENT_ID, 67 | clientSecret: DEV_CONFIG.GITHUB_CLIENT_SECRET_ID, 68 | callbackURL: "/auth/google/callback" 69 | }, 70 | (accessToken, refreshToken, profile, done) => { 71 | console.log("=======Google profile========"); 72 | console.log(profile); 73 | console.log("==============="); 74 | user_service 75 | .findOne({ 76 | email: profile.email, 77 | name: profile.username, 78 | google_id: profile.id 79 | }) 80 | .then(existing_user => { 81 | if (existing_user) { 82 | done(null, existing_user); 83 | } else { 84 | user_service 85 | .createdUserWithOauth2({ 86 | email: profile.email, 87 | name: profile.username, 88 | google_id: profile.id 89 | }) 90 | .then(user => { 91 | done(null, user); 92 | }); 93 | } 94 | }); 95 | } 96 | ) 97 | ); 98 | 99 | // Facebook 100 | passport.use( 101 | new facebook.Strategy( 102 | { 103 | clientID: DEV_CONFIG.GITHUB_CLIENT_ID, 104 | clientSecret: DEV_CONFIG.GITHUB_CLIENT_SECRET_ID, 105 | callbackURL: "/auth/facebook/callback" 106 | }, 107 | (accessToken, refreshToken, profile, done) => { 108 | console.log("=======Facebook profile========"); 109 | console.log(profile); 110 | console.log("==============="); 111 | user_service 112 | .findOne({ 113 | email: profile.email, 114 | name: profile.username, 115 | facebook_id: profile.id 116 | }) 117 | .then(existing_user => { 118 | if (existing_user) { 119 | done(null, existing_user); 120 | } else { 121 | user_service 122 | .createdUser({ 123 | email: profile.email, 124 | name: profile.username, 125 | facebook_id: profile.id 126 | }) 127 | .then(user => { 128 | done(null, user); 129 | }); 130 | } 131 | }); 132 | } 133 | ) 134 | ); 135 | 136 | export { passport }; 137 | -------------------------------------------------------------------------------- /client/src/assets/scss/variable.scss: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------------------------ 2 | - Global Color Variable 3 | -------------------------------------------------------------------------- */ 4 | $black: #232323; 5 | $light-black: lighten($black, 15%); 6 | $dark-black: darken($black, 2%); 7 | $darker-black: darken($dark-black, 3%); 8 | // ---- 9 | $white: #ffffff; 10 | $light-white: lighten($white, 15%); 11 | $dark-white: darken($white, 2%); 12 | $darker-white: darken($dark-white, 3%); 13 | // ---- 14 | $gray: #868e96; 15 | $light-gray: lighten($gray, 15%); 16 | $dark-gray: darken($gray, 2%); 17 | $darker-gray: darken($dark-gray, 3%); 18 | // ---- 19 | $ocean: #0080ff; 20 | $light-ocean: lighten($ocean, 15%); 21 | $dark-ocean: darken($ocean, 2%); 22 | $darker-ocean: darken($dark-ocean, 3%); 23 | // ---- 24 | $royal-blue: royalblue; 25 | $light-royal-blue: lighten($royal-blue, 15%); 26 | $dark-royal-blue: darken($royal-blue, 2%); 27 | $darker-royal-blue: darken($dark-royal-blue, 3%); 28 | /* ------------------------ 29 | - Dom Css Using Global Color Variable 30 | # Background And Active Dom Color Variable using $Color Variable 31 | # 'Active Dom' are Input, Button and another elements. 32 | ------------------------ */ 33 | 34 | // Background Color 35 | $bg-color: $white !default; 36 | $bg-font-color: $light-black; 37 | $bg-color-hover: $light-white; 38 | $bg-color-shadow: $dark-white; 39 | // Active Color 40 | $active-color: $ocean !default; 41 | $active-font-color: $white !default; 42 | $active-color-hover: $light-ocean !default; 43 | $active-color-shadow: $darker-ocean !default; 44 | // Active2 Color 45 | $active2-color: $royal-blue !default; 46 | $active2-font-color: $white !default; 47 | $active2-color-hover: $light-royal-blue !default; 48 | $active2-color-shadow: $darker-royal-blue !default; 49 | // Sub-Function Color 50 | $sub-active-color: $gray !default; 51 | $sub-active-font-color: $black !default; 52 | $sub-active-color-hover: $light-gray !default; 53 | $sub-active-color-shadow: $darker-gray !default; 54 | /* ------------------------ 55 | - Often Used color 56 | ------------------------ */ 57 | 58 | $colors: ( 59 | "black": #000000, 60 | "white": #fff, 61 | "gray": #808080, 62 | "darkgray": #a9a9a9, 63 | "red": #ff0000, 64 | "crimson": #dc143c, 65 | "aqua": #00ffff, 66 | "aquamarine": #7fffd4, 67 | "orange": #ffa500, 68 | "orangered": #ff4500, 69 | "green": #008000, 70 | "limegreen": #32cd32, 71 | "lightgreen": #90ee90, 72 | "yellow": #ffff00, 73 | "blue": #0000ff, 74 | "royalblue": #4169e1, 75 | "purple": #800080, 76 | "ocean": #0080ff 77 | ); 78 | /* ------------------------ 79 | - Google Font 80 | ------------------------ */ 81 | 82 | @mixin font-face( 83 | $name, 84 | $font-files, 85 | $eot: false, 86 | $weight: false, 87 | $style: false 88 | ) { 89 | $iefont: unquote("#{$eot}?#iefix"); 90 | @font-face { 91 | font-family: quote($name); 92 | @if $eot { 93 | src: font-url($eot); 94 | $font-files: font-url($iefont) unquote("format('eot')"), $font-files; 95 | } 96 | src: $font-files; 97 | @if $weight { 98 | font-weight: $weight; 99 | } 100 | @if $style { 101 | font-style: $style; 102 | } 103 | } 104 | } 105 | 106 | $font-main-1: "Lato", sans-serif; 107 | $font-main-2: "Open Sans", sans-serif; 108 | $font-main-3: "Droid Sans", sans-serif; 109 | $font-main-4: "Noto Sans", sans-serif; 110 | @font-face { 111 | font-family: $font-main-1; 112 | src: url("https://fonts.googleapis.com/css?family=Droid+Sans|Lato|Noto+Sans|Open+Sans"); 113 | } 114 | 115 | /* ------------------------------------------------------------------------ 116 | - Global Boolean Variable 117 | -------------------------------------------------------------------------- */ 118 | 119 | $boolean: true !default; 120 | /* ------------------------------------------------------------------------ 121 | - Global Grid Size Variable 122 | -------------------------------------------------------------------------- */ 123 | 124 | $header-height: 80px; 125 | /* ------------------------ 126 | - Media Size Variable 127 | ------------------------ */ 128 | 129 | $media-medium: 1024px; 130 | $media-small: 640px; 131 | -------------------------------------------------------------------------------- /server/src/app/services/UserService.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../types"; 2 | import { PasswordEncoderUtils } from "../utils/PasswordEncoderUtils"; 3 | import { UserRepository } from "./repository"; 4 | import { Order } from "./repository/AbstractRepository"; 5 | 6 | const userRepository = new UserRepository([ 7 | "id", 8 | "email", 9 | "name", 10 | "github_id", 11 | "google_id", 12 | "facebook_id" 13 | ]); 14 | class UserService { 15 | // Basic 16 | createdUser = async ({ 17 | email, 18 | name, 19 | birth, 20 | password, 21 | division_id, 22 | google_id, 23 | github_id, 24 | facebook_id 25 | }: User) => { 26 | if (!email) { 27 | return new Error("The email is requirement."); 28 | } 29 | if (!name) { 30 | return new Error("The name are requirement."); 31 | } 32 | if (!birth) { 33 | return new Error("The birth are requirement."); 34 | } 35 | if (!password) { 36 | return new Error("The password are requirement."); 37 | } 38 | if (!division_id) { 39 | return new Error("The division_id are requirement."); 40 | } 41 | 42 | const dbUser = await userRepository.findOne({ email, name }); 43 | if (dbUser) { 44 | if (dbUser.get("email") === email) { 45 | return new Error(`Already the '${email}' are exists.`); 46 | } 47 | if (dbUser.get("name") === name) { 48 | return new Error(`Already the '${name}' are exists.`); 49 | } 50 | } 51 | 52 | const bcryptedPassword = PasswordEncoderUtils.bcryptedPasswordSync( 53 | password 54 | ); 55 | return userRepository.create({ 56 | email, 57 | name, 58 | birth, 59 | password: bcryptedPassword, 60 | division_id, 61 | google_id, 62 | github_id, 63 | facebook_id 64 | }); 65 | }; 66 | 67 | findOne({ id, email, name, google_id, github_id, facebook_id }: User) { 68 | if (!id && !email && !name && !google_id && !github_id && !facebook_id) { 69 | return Promise.reject( 70 | new Error("One of id and email, name are requirement.") 71 | ); 72 | } 73 | return userRepository.findOne({ 74 | id, 75 | email, 76 | name, 77 | google_id, 78 | github_id, 79 | facebook_id 80 | }); 81 | } 82 | 83 | findAll(order: Order) { 84 | return userRepository.findAll("DESC"); 85 | } 86 | 87 | updatedUser({ id, email, birth, name, division_id }: User) { 88 | if (!id && !email) { 89 | return Promise.reject(new Error("id or email are requirement.")); 90 | } 91 | 92 | return userRepository.findOne({ id, email }).then(dbUser => { 93 | if (!dbUser) { 94 | return Promise.reject(new Error("The user are not found")); 95 | } 96 | return userRepository.update({ email, birth, name, division_id }); 97 | }); 98 | } 99 | 100 | deletedUser({ id, email }: User) { 101 | if (!id && !email) { 102 | return Promise.reject(new Error(`id or email are requirement.`)); 103 | } 104 | return userRepository.findOne({ id, email }).then(dbUser => { 105 | if (!dbUser) { 106 | return Promise.reject(new Error("The user are not found")); 107 | } 108 | return userRepository.delete({ id, email }); 109 | }); 110 | } 111 | 112 | // Custom 113 | createdUserWithOauth2({ 114 | email, 115 | name, 116 | division_id, 117 | google_id, 118 | github_id, 119 | facebook_id 120 | }: User) { 121 | if (!name) { 122 | return Promise.reject(new Error("The name are requirement.")); 123 | } 124 | 125 | return userRepository 126 | .findOne({ email, name, google_id, github_id, facebook_id }) 127 | .then(dbUser => { 128 | if (dbUser) { 129 | return Promise.resolve(dbUser); 130 | } 131 | return userRepository.create({ 132 | email, 133 | name, 134 | division_id, 135 | google_id, 136 | github_id, 137 | facebook_id 138 | }); 139 | }); 140 | } 141 | 142 | loginUser({ email, name, password }: User) { 143 | if (!email && !name) { 144 | return Promise.reject( 145 | new Error("One of email and name are requirement.") 146 | ); 147 | } 148 | 149 | let password_validation = false; 150 | return userRepository.findOne({ email, name }).then((dbUser: any) => { 151 | if (password) { 152 | password_validation = PasswordEncoderUtils.compareBcryptedPasswordSync( 153 | password, 154 | dbUser.password 155 | ); 156 | if (!password_validation) { 157 | return Promise.reject( 158 | new Error("The identification or Password is not right") 159 | ); 160 | } 161 | return Promise.resolve(dbUser); 162 | } 163 | return Promise.reject( 164 | new Error("The identification or Password is not right") 165 | ); 166 | }); 167 | } 168 | } 169 | 170 | export { UserService }; 171 | -------------------------------------------------------------------------------- /client/src/components/ui/auth/SigninView.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 233 | 234 | 237 | -------------------------------------------------------------------------------- /client/src/assets/scss/global.scss: -------------------------------------------------------------------------------- 1 | @import "variable"; 2 | /* ------------------------------------------------ 3 | Global Tag CSS 4 | ------------------------------------------------ */ 5 | 6 | html, 7 | body { 8 | height: 100%; 9 | } 10 | 11 | // Common Body 12 | body { 13 | background: $bg-color; 14 | font-family: "Avenir", Helvetica, Arial, sans-serif; 15 | } 16 | 17 | main#hunseol { 18 | min-height: calc(100vh - 18.5rem); 19 | } 20 | 21 | a { 22 | color: $active-color; 23 | &:active { 24 | color: $active-color-hover; 25 | } 26 | } 27 | 28 | // Button Css 29 | button { 30 | cursor: pointer; 31 | display: inline-block; 32 | font-weight: 400; 33 | text-align: center; 34 | white-space: nowrap; 35 | vertical-align: middle; 36 | user-select: none; 37 | border: 1px solid transparent; 38 | padding: 0.375rem 0.75rem; 39 | font-size: 1rem; 40 | line-height: 1.5; 41 | border-radius: 0.25rem; 42 | transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, 43 | box-shadow 0.15s ease-in-out; 44 | -webkit-user-select: none; 45 | -moz-user-select: none; 46 | -ms-user-select: none; 47 | } 48 | 49 | textarea.auto-size { 50 | width: 100%; 51 | height: 100%; 52 | overflow: scroll; 53 | min-height: 300px; 54 | box-sizing: border-box; 55 | line-height: 1.5; 56 | padding: 15px 15px; 57 | border-radius: 3px; 58 | transition: box-shadow 0.5s ease; 59 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 60 | } 61 | 62 | /* ------------------------ 63 | Global ID CSS 64 | ------------------------ */ 65 | 66 | #app { 67 | font-family: $font-main-1; 68 | -webkit-font-smoothing: antialiased; 69 | -moz-osx-font-smoothing: grayscale; 70 | } 71 | 72 | /* ------------------------------------------------------------------------ 73 | - Global For Active SCSS Start 74 | -------------------------------------------------------------------------- */ 75 | 76 | /* ------------------------ 77 | Tag h1 ~ h5 CSS 78 | ------------------------ */ 79 | 80 | $h-size: h !default; 81 | @for $i from 1 through 6 { 82 | #{$h-size}#{$i} { 83 | font-weight: 600; 84 | font-size: 10px + (30 / $i); 85 | padding: 15px 0px; 86 | word-break: break-word; 87 | } 88 | } 89 | 90 | /* ---------------------------------------- 91 | - Class Variable CSS 92 | ------------------------------------------- */ 93 | 94 | // Sub Active Btn 95 | /* ------------------------------------------------ 96 | Colorful Btn - BackGround CSS 97 | ------------------------------------------------ */ 98 | 99 | @each $color in $colors { 100 | $key: nth($color, 1); 101 | $value: nth($color, 2); 102 | .#{"btn-"}#{$key} { 103 | color: $white; 104 | background-color: $value; 105 | &:hover { 106 | background: darken($value, 5%); 107 | font-weight: bold; 108 | } 109 | &:active { 110 | background-color: darken($white, 30%); 111 | } 112 | &:disabled { 113 | cursor: not-allowed; 114 | background: #dddddd; 115 | } 116 | } 117 | .#{"btn-edge-"}#{$key} { 118 | border-color: $value; 119 | color: $value; 120 | background-color: $white; 121 | &:hover { 122 | background: darken($value, 5%); 123 | color: darken($white, 5%); 124 | font-weight: 800; 125 | } 126 | &:active { 127 | background-color: darken($white, 30%); 128 | } 129 | &:disabled { 130 | cursor: not-allowed; 131 | background: #dddddd; 132 | } 133 | } 134 | .#{"background-"}#{$key} { 135 | color: $white; 136 | background: $value; 137 | } 138 | } 139 | 140 | /* ------------------------------------------------ 141 | Global Class CSS 142 | ------------------------------------------------ */ 143 | 144 | /* ---------------- 145 | Vue Router SCSS 146 | ------------------*/ 147 | 148 | .router-link-exact-active { 149 | font-weight: 700; 150 | color: $light-white; 151 | } 152 | 153 | .router-item { 154 | cursor: pointer; 155 | text-decoration: none; 156 | &:hover { 157 | text-decoration: none; 158 | } 159 | } 160 | 161 | /* ------------------------ 162 | Text-alignment 163 | ------------------------ */ 164 | 165 | .text-right { 166 | text-align: right; 167 | } 168 | 169 | .text-left { 170 | text-align: left; 171 | } 172 | 173 | .text-center { 174 | text-align: center; 175 | } 176 | 177 | /* ------------------------ 178 | Width Coverage 179 | # .width-1 ~ width-100 css 180 | ------------------------ */ 181 | 182 | $width-class: width- !default; 183 | @for $i from 1 through 20 { 184 | .#{$width-class}#{$i * 5} { 185 | width: 0% + ($i * 5); 186 | } 187 | } 188 | 189 | /* ------------------------ 190 | Height Coverage 191 | # .Height-50 ~ Height-500 css 192 | ------------------------ */ 193 | 194 | $height: height- !default; 195 | @for $i from 1 through 20 { 196 | .#{$height}#{$i * 50} { 197 | height: 0px + ($i * 50); 198 | word-break: keep-all; 199 | } 200 | } 201 | 202 | $max-height: max-height- !default; 203 | @for $i from 1 through 20 { 204 | .#{$max-height}#{$i*50} { 205 | max-height: 0px + ($i * 50); 206 | word-break: keep-all; 207 | min-height: 0px + ($i * 10); 208 | height: auto; 209 | } 210 | } 211 | 212 | $min-height: min-height- !default; 213 | @for $i from 1 through 20 { 214 | .#{$min-height}#{$i*50} { 215 | min-height: 0px + ($i * 50); 216 | word-break: keep-all; 217 | height: auto; 218 | overflow-y: auto; 219 | } 220 | } 221 | 222 | /* ------------------------ 223 | Font Weight & Size 224 | # .font-size-1 ~ 225 | # .font-weight-1 ~ 226 | ------------------------ */ 227 | 228 | $font-size: font-size- !default; 229 | @for $i from 10 through 100 { 230 | .#{$font-size}#{$i} { 231 | font-size: 0px + ($i); 232 | } 233 | } 234 | 235 | $font-weight: font-weight- !default; 236 | @for $i from 1 through 10 { 237 | .#{$font-weight}#{$i} { 238 | font-weight: 100 * ($i); 239 | } 240 | } 241 | 242 | /* ------------------------ 243 | padding-10 ~ padding-60 CSS 244 | ------------------------ */ 245 | 246 | $padding: padding- !default; 247 | @for $i from 1 through 6 { 248 | .#{$padding}#{$i*10} { 249 | padding: (10px * $i); 250 | } 251 | } 252 | 253 | $padding-top: padding-top- !default; 254 | @for $i from 1 through 20 { 255 | .#{$padding-top}#{$i*10} { 256 | padding-top: (5px * $i) 0px 0px 0px; 257 | } 258 | } 259 | 260 | $padding-bottom: padding-bottom- !default; 261 | @for $i from 1 through 20 { 262 | .#{$padding-bottom}#{$i*10} { 263 | padding-bottom: 0px 0px (5px * $i) 0px; 264 | } 265 | } 266 | 267 | $padding-right: padding-right- !default; 268 | @for $i from 1 through 20 { 269 | .#{$padding-right}#{$i*10} { 270 | padding-right: 0px 0px (5px * $i) 0px; 271 | } 272 | } 273 | 274 | $padding-left: padding-left- !default; 275 | @for $i from 1 through 20 { 276 | .#{$padding-left}#{$i*10} { 277 | padding-left: 0px 0px (5px * $i) 0px; 278 | } 279 | } 280 | 281 | /* ------------------------ 282 | margin-10 ~ margin-60 CSS 283 | ------------------------ */ 284 | 285 | $margin: margin- !default; 286 | @for $i from 1 through 20 { 287 | .#{$margin}#{$i * 5} { 288 | margin: (5px * $i); 289 | } 290 | } 291 | 292 | $margin-top: margin-top- !default; 293 | @for $i from 1 through 20 { 294 | .#{$margin-top}#{$i * 5} { 295 | margin-top: (5px * $i); 296 | } 297 | } 298 | 299 | $margin-bottom: margin-bottom- !default; 300 | @for $i from 1 through 20 { 301 | .#{$margin-bottom}#{$i * 5} { 302 | margin-bottom: (5px * $i); 303 | } 304 | } 305 | 306 | $margin-right: margin-right- !default; 307 | @for $i from 1 through 20 { 308 | .#{$margin-right}#{$i * 5} { 309 | margin-right: (5px * $i); 310 | } 311 | } 312 | 313 | $margin-left: margin-left- !default; 314 | @for $i from 1 through 20 { 315 | .#{$margin-left}#{$i * 5} { 316 | margin-left: (5px * $i); 317 | } 318 | } 319 | 320 | /* ------------------------ 321 | Label Title CSS 322 | ------------------------ */ 323 | 324 | $label-title: label-title- !default; 325 | @for $i from 12 through 30 { 326 | .#{$label-title}#{$i} { 327 | font-size: 1px * $i; 328 | font-weight: 600; 329 | word-break: break-word; 330 | } 331 | } 332 | 333 | /* ------------------------ 334 | Shadow Box CSS 335 | ------------------------ */ 336 | 337 | $shadow-box: shadow-box- !default; 338 | @for $i from 1 through 10 { 339 | .#{$shadow-box}#{$i} { 340 | -webkit-box-shadow: 0 341 | (15px + $i) 342 | (20px + $i) 343 | (-10px - $i) 344 | $gray; /* Safari 3-4, iOS 4.0.2 - 4.2, Android 2.3+ */ 345 | -moz-box-shadow: 0 346 | (15px + $i) 347 | (20px + $i) 348 | (-10px - $i) 349 | $gray; /* Firefox 3.5 - 3.6 */ 350 | box-shadow: 0 351 | (15px + $i) 352 | (20px + $i) 353 | (-10px - $i) 354 | $gray; /* Opera 10.5, IE 9, Firefox 4+, Chrome 6+, iOS 5 */ 355 | padding: 20px; 356 | } 357 | } 358 | 359 | /* ------------------------ 360 | Bootstrap Table 361 | ------------------------ */ 362 | 363 | .table-hover tr:hover { 364 | cursor: pointer; 365 | } 366 | -------------------------------------------------------------------------------- /client/public/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /docker-elk/README.md: -------------------------------------------------------------------------------- 1 | # Docker ELK stack 2 | 3 | [![Join the chat at https://gitter.im/deviantony/docker-elk](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/deviantony/docker-elk?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Elastic Stack version](https://img.shields.io/badge/ELK-6.2.2-blue.svg?style=flat)](https://github.com/deviantony/docker-elk/issues/248) 5 | [![Build Status](https://api.travis-ci.org/deviantony/docker-elk.svg?branch=master)](https://travis-ci.org/deviantony/docker-elk) 6 | 7 | Run the latest version of the ELK (Elasticsearch, Logstash, Kibana) stack with Docker and Docker Compose. 8 | 9 | It will give you the ability to analyze any data set by using the searching/aggregation capabilities of Elasticsearch 10 | and the visualization power of Kibana. 11 | 12 | Based on the official Docker images: 13 | 14 | * [elasticsearch](https://github.com/elastic/elasticsearch-docker) 15 | * [logstash](https://github.com/elastic/logstash-docker) 16 | * [kibana](https://github.com/elastic/kibana-docker) 17 | 18 | **Note**: Other branches in this project are available: 19 | 20 | * ELK 6 with X-Pack support: https://github.com/deviantony/docker-elk/tree/x-pack 21 | * ELK 6 in Vagrant: https://github.com/deviantony/docker-elk/tree/vagrant 22 | * ELK 6 with Search Guard: https://github.com/deviantony/docker-elk/tree/searchguard 23 | 24 | ## Contents 25 | 26 | 1. [Requirements](#requirements) 27 | * [Host setup](#host-setup) 28 | * [SELinux](#selinux) 29 | * [DockerForWindows](#dockerforwindows) 30 | 2. [Getting started](#getting-started) 31 | * [Bringing up the stack](#bringing-up-the-stack) 32 | * [Initial setup](#initial-setup) 33 | 3. [Configuration](#configuration) 34 | * [How can I tune the Kibana configuration?](#how-can-i-tune-the-kibana-configuration) 35 | * [How can I tune the Logstash configuration?](#how-can-i-tune-the-logstash-configuration) 36 | * [How can I tune the Elasticsearch configuration?](#how-can-i-tune-the-elasticsearch-configuration) 37 | * [How can I scale out the Elasticsearch cluster?](#how-can-i-scale-up-the-elasticsearch-cluster) 38 | 4. [Storage](#storage) 39 | * [How can I persist Elasticsearch data?](#how-can-i-persist-elasticsearch-data) 40 | 5. [Extensibility](#extensibility) 41 | * [How can I add plugins?](#how-can-i-add-plugins) 42 | * [How can I enable the provided extensions?](#how-can-i-enable-the-provided-extensions) 43 | 6. [JVM tuning](#jvm-tuning) 44 | * [How can I specify the amount of memory used by a service?](#how-can-i-specify-the-amount-of-memory-used-by-a-service) 45 | * [How can I enable a remote JMX connection to a service?](#how-can-i-enable-a-remote-jmx-connection-to-a-service) 46 | 47 | ## Requirements 48 | 49 | ### Host setup 50 | 51 | 1. Install [Docker](https://www.docker.com/community-edition#/download) version **1.10.0+** 52 | 2. Install [Docker Compose](https://docs.docker.com/compose/install/) version **1.6.0+** 53 | 3. Clone this repository 54 | 55 | ### SELinux 56 | 57 | On distributions which have SELinux enabled out-of-the-box you will need to either re-context the files or set SELinux 58 | into Permissive mode in order for docker-elk to start properly. For example on Redhat and CentOS, the following will 59 | apply the proper context: 60 | 61 | ```console 62 | $ chcon -R system_u:object_r:admin_home_t:s0 docker-elk/ 63 | ``` 64 | 65 | ### DockerForWindows 66 | 67 | If you're using Docker for Windows, ensure the 'Shared Drives' feature is enabled for the C: drive (Docker for Windows > Settings > Shared Drives). [MSDN article detailing Shared Drives config](https://blogs.msdn.microsoft.com/stevelasker/2016/06/14/configuring-docker-for-windows-volumes/). 68 | 69 | ## Usage 70 | 71 | ### Bringing up the stack 72 | 73 | **Note**: In case you switched branch or updated a base image - you may need to run `docker-compose build` first 74 | 75 | Start the ELK stack using `docker-compose`: 76 | 77 | ```console 78 | $ docker-compose up 79 | ``` 80 | 81 | You can also choose to run it in background (detached mode): 82 | 83 | ```console 84 | $ docker-compose up -d 85 | ``` 86 | 87 | Give Kibana a few seconds to initialize, then access the Kibana web UI by hitting 88 | [http://localhost:5601](http://localhost:5601) with a web browser. 89 | 90 | By default, the stack exposes the following ports: 91 | * 5000: Logstash TCP input. 92 | * 9200: Elasticsearch HTTP 93 | * 9300: Elasticsearch TCP transport 94 | * 5601: Kibana 95 | 96 | **WARNING**: If you're using `boot2docker`, you must access it via the `boot2docker` IP address instead of `localhost`. 97 | 98 | **WARNING**: If you're using *Docker Toolbox*, you must access it via the `docker-machine` IP address instead of 99 | `localhost`. 100 | 101 | Now that the stack is running, you will want to inject some log entries. The shipped Logstash configuration allows you 102 | to send content via TCP: 103 | 104 | ```console 105 | $ nc localhost 5000 < /path/to/logfile.log 106 | ``` 107 | 108 | ## Initial setup 109 | 110 | ### Default Kibana index pattern creation 111 | 112 | When Kibana launches for the first time, it is not configured with any index pattern. 113 | 114 | #### Via the Kibana web UI 115 | 116 | **NOTE**: You need to inject data into Logstash before being able to configure a Logstash index pattern via the Kibana web 117 | UI. Then all you have to do is hit the *Create* button. 118 | 119 | Refer to [Connect Kibana with 120 | Elasticsearch](https://www.elastic.co/guide/en/kibana/current/connect-to-elasticsearch.html) for detailed instructions 121 | about the index pattern configuration. 122 | 123 | #### On the command line 124 | 125 | Create an index pattern via the Kibana API: 126 | 127 | ```console 128 | $ curl -XPOST -D- 'http://localhost:5601/api/saved_objects/index-pattern' \ 129 | -H 'Content-Type: application/json' \ 130 | -H 'kbn-version: 6.2.2' \ 131 | -d '{"attributes":{"title":"logstash-*","timeFieldName":"@timestamp"}}' 132 | ``` 133 | 134 | The created pattern will automatically be marked as the default index pattern as soon as the Kibana UI is opened for the first time. 135 | 136 | ## Configuration 137 | 138 | **NOTE**: Configuration is not dynamically reloaded, you will need to restart the stack after any change in the 139 | configuration of a component. 140 | 141 | ### How can I tune the Kibana configuration? 142 | 143 | The Kibana default configuration is stored in `kibana/config/kibana.yml`. 144 | 145 | It is also possible to map the entire `config` directory instead of a single file. 146 | 147 | ### How can I tune the Logstash configuration? 148 | 149 | The Logstash configuration is stored in `logstash/config/logstash.yml`. 150 | 151 | It is also possible to map the entire `config` directory instead of a single file, however you must be aware that 152 | Logstash will be expecting a 153 | [`log4j2.properties`](https://github.com/elastic/logstash-docker/tree/master/build/logstash/config) file for its own 154 | logging. 155 | 156 | ### How can I tune the Elasticsearch configuration? 157 | 158 | The Elasticsearch configuration is stored in `elasticsearch/config/elasticsearch.yml`. 159 | 160 | You can also specify the options you want to override directly via environment variables: 161 | 162 | ```yml 163 | elasticsearch: 164 | 165 | environment: 166 | network.host: "_non_loopback_" 167 | cluster.name: "my-cluster" 168 | ``` 169 | 170 | ### How can I scale out the Elasticsearch cluster? 171 | 172 | Follow the instructions from the Wiki: [Scaling out 173 | Elasticsearch](https://github.com/deviantony/docker-elk/wiki/Elasticsearch-cluster) 174 | 175 | ## Storage 176 | 177 | ### How can I persist Elasticsearch data? 178 | 179 | The data stored in Elasticsearch will be persisted after container reboot but not after container removal. 180 | 181 | In order to persist Elasticsearch data even after removing the Elasticsearch container, you'll have to mount a volume on 182 | your Docker host. Update the `elasticsearch` service declaration to: 183 | 184 | ```yml 185 | elasticsearch: 186 | 187 | volumes: 188 | - /path/to/storage:/usr/share/elasticsearch/data 189 | ``` 190 | 191 | This will store Elasticsearch data inside `/path/to/storage`. 192 | 193 | **NOTE:** beware of these OS-specific considerations: 194 | * **Linux:** the [unprivileged `elasticsearch` user][esuser] is used within the Elasticsearch image, therefore the 195 | mounted data directory must be owned by the uid `1000`. 196 | * **macOS:** the default Docker for Mac configuration allows mounting files from `/Users/`, `/Volumes/`, `/private/`, 197 | and `/tmp` exclusively. Follow the instructions from the [documentation][macmounts] to add more locations. 198 | 199 | [esuser]: https://github.com/elastic/elasticsearch-docker/blob/016bcc9db1dd97ecd0ff60c1290e7fa9142f8ddd/templates/Dockerfile.j2#L22 200 | [macmounts]: https://docs.docker.com/docker-for-mac/osxfs/ 201 | 202 | ## Extensibility 203 | 204 | ### How can I add plugins? 205 | 206 | To add plugins to any ELK component you have to: 207 | 208 | 1. Add a `RUN` statement to the corresponding `Dockerfile` (eg. `RUN logstash-plugin install logstash-filter-json`) 209 | 2. Add the associated plugin code configuration to the service configuration (eg. Logstash input/output) 210 | 3. Rebuild the images using the `docker-compose build` command 211 | 212 | ### How can I enable the provided extensions? 213 | 214 | A few extensions are available inside the [`extensions`](extensions) directory. These extensions provide features which 215 | are not part of the standard Elastic stack, but can be used to enrich it with extra integrations. 216 | 217 | The documentation for these extensions is provided inside each individual subdirectory, on a per-extension basis. Some 218 | of them require manual changes to the default ELK configuration. 219 | 220 | ## JVM tuning 221 | 222 | ### How can I specify the amount of memory used by a service? 223 | 224 | By default, both Elasticsearch and Logstash start with [1/4 of the total host 225 | memory](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/parallel.html#default_heap_size) allocated to 226 | the JVM Heap Size. 227 | 228 | The startup scripts for Elasticsearch and Logstash can append extra JVM options from the value of an environment 229 | variable, allowing the user to adjust the amount of memory that can be used by each component: 230 | 231 | | Service | Environment variable | 232 | |---------------|----------------------| 233 | | Elasticsearch | ES_JAVA_OPTS | 234 | | Logstash | LS_JAVA_OPTS | 235 | 236 | To accomodate environments where memory is scarce (Docker for Mac has only 2 GB available by default), the Heap Size 237 | allocation is capped by default to 256MB per service in the `docker-compose.yml` file. If you want to override the 238 | default JVM configuration, edit the matching environment variable(s) in the `docker-compose.yml` file. 239 | 240 | For example, to increase the maximum JVM Heap Size for Logstash: 241 | 242 | ```yml 243 | logstash: 244 | 245 | environment: 246 | LS_JAVA_OPTS: "-Xmx1g -Xms1g" 247 | ``` 248 | 249 | ### How can I enable a remote JMX connection to a service? 250 | 251 | As for the Java Heap memory (see above), you can specify JVM options to enable JMX and map the JMX port on the docker 252 | host. 253 | 254 | Update the `{ES,LS}_JAVA_OPTS` environment variable with the following content (I've mapped the JMX service on the port 255 | 18080, you can change that). Do not forget to update the `-Djava.rmi.server.hostname` option with the IP address of your 256 | Docker host (replace **DOCKER_HOST_IP**): 257 | 258 | ```yml 259 | logstash: 260 | 261 | environment: 262 | LS_JAVA_OPTS: "-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.port=18080 -Dcom.sun.management.jmxremote.rmi.port=18080 -Djava.rmi.server.hostname=DOCKER_HOST_IP -Dcom.sun.management.jmxremote.local.only=false" 263 | ``` 264 | --------------------------------------------------------------------------------