? U : T
12 |
--------------------------------------------------------------------------------
/packages/fixtures/src/utility/index.ts:
--------------------------------------------------------------------------------
1 | export * from './hooks'
2 | export * from './ssr'
3 |
--------------------------------------------------------------------------------
/packages/fixtures/src/utility/ssr.ts:
--------------------------------------------------------------------------------
1 | export const isBrowser = () => typeof window !== 'undefined'
2 |
--------------------------------------------------------------------------------
/packages/fixtures/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "target": "es5",
5 | "lib": ["dom", "es2018"],
6 | "strict": true,
7 | "strictNullChecks": true,
8 | "noImplicitAny": true,
9 | "noImplicitReturns": true,
10 | "skipLibCheck": true,
11 | "declaration": true,
12 | "declarationMap": true,
13 | "pretty": true,
14 | "newLine": "lf",
15 | "esModuleInterop": true,
16 | "moduleResolution": "node"
17 | },
18 | "include": ["../fixtures/src/types/*.d.ts", "**/*.ts", "**/*.tsx"]
19 | }
20 |
--------------------------------------------------------------------------------
/packages/graphql/.gitignore:
--------------------------------------------------------------------------------
1 | cjs
2 | esm
--------------------------------------------------------------------------------
/packages/graphql/codegen.yml:
--------------------------------------------------------------------------------
1 | overwrite: true
2 | schema: '../backend-graphql/src/schema.graphql'
3 | documents: ['src/queries/**.graphql', 'src/mutations/**.graphql', 'src/fragments/**.graphql']
4 | generates:
5 | src/react-apollo/generated.tsx:
6 | plugins:
7 | - 'typescript'
8 | - 'typescript-operations'
9 | - 'typescript-react-apollo'
10 | config:
11 | withComponent: false
12 | withHooks: true
13 | withHOC: false
14 | maybeValue: T
15 |
--------------------------------------------------------------------------------
/packages/graphql/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@abyssparanoia/graphql",
3 | "version": "0.0.1",
4 | "private": true,
5 | "main": "cjs/index.js",
6 | "module": "esm/index.js",
7 | "typings": "esm/index.d.ts",
8 | "scripts": {
9 | "build": "graphql-codegen --config codegen.yml && run-p -l build:* && yarn format",
10 | "build:cjs": "tsc --project . --module commonjs --outDir ./cjs",
11 | "build:esm": "tsc --project . --module es2015 --outDir ./esm",
12 | "clean": "rimraf esm cjs",
13 | "lint": "tsc -p . --noEmit",
14 | "format": "prettier --write \"src/**/*.{ts,tsx}\"",
15 | "prebuild": "yarn clean"
16 | },
17 | "dependencies": {
18 | "@graphql-codegen/cli": "2.3.0",
19 | "@graphql-codegen/typescript": "2.4.0",
20 | "@graphql-codegen/typescript-operations": "2.2.0",
21 | "@graphql-codegen/typescript-react-apollo": "3.2.1",
22 | "react-apollo": "3.1.5"
23 | },
24 | "devDependencies": {
25 | "@graphql-codegen/cli": "2.3.0",
26 | "@graphql-codegen/introspection": "2.1.0",
27 | "@graphql-codegen/typescript": "2.4.0",
28 | "@graphql-codegen/typescript-operations": "2.2.0",
29 | "@graphql-codegen/typescript-react-apollo": "3.2.1",
30 | "@graphql-codegen/typescript-resolvers": "2.4.1"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/graphql/src/fragments/user.graphql:
--------------------------------------------------------------------------------
1 | fragment user on User {
2 | id
3 | name
4 | }
5 |
--------------------------------------------------------------------------------
/packages/graphql/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './react-apollo/generated'
2 |
--------------------------------------------------------------------------------
/packages/graphql/src/mutations/createUser.graphql:
--------------------------------------------------------------------------------
1 | mutation CreateUser($param: UserCreateInput!) {
2 | createUser(param: $param) {
3 | ...user
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/packages/graphql/src/queries/listUsers.graphql:
--------------------------------------------------------------------------------
1 | # Write your query or mutation here
2 | query ListUsers {
3 | list {
4 | ...user
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/packages/graphql/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "target": "es5",
5 | "lib": ["dom", "es2018"],
6 | "strict": true,
7 | "strictNullChecks": true,
8 | "noImplicitAny": true,
9 | "noImplicitReturns": true,
10 | "skipLibCheck": true,
11 | "declaration": true,
12 | "declarationMap": true,
13 | "pretty": true,
14 | "newLine": "lf",
15 | "esModuleInterop": true,
16 | "moduleResolution": "node",
17 | "jsx": "react"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/interface/.gitignore:
--------------------------------------------------------------------------------
1 | cjs
2 | esm
--------------------------------------------------------------------------------
/packages/interface/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@abyssparanoia/interface",
3 | "version": "0.0.1",
4 | "private": true,
5 | "main": "cjs/index.js",
6 | "module": "esm/index.js",
7 | "typings": "esm/index.d.ts",
8 | "scripts": {
9 | "build": "run-p -l build:*",
10 | "build:cjs": "tsc --project . --module commonjs --outDir ./cjs",
11 | "build:esm": "tsc --project . --module es2015 --outDir ./esm",
12 | "clean": "rimraf esm cjs",
13 | "lint": "tsc -p . --noEmit",
14 | "prebuild": "yarn clean"
15 | },
16 | "dependencies": {}
17 | }
18 |
--------------------------------------------------------------------------------
/packages/interface/src/entity/claims.ts:
--------------------------------------------------------------------------------
1 | export type Role = 'admin' | 'member'
2 |
3 | export interface Claims {
4 | role: Role
5 | }
6 |
--------------------------------------------------------------------------------
/packages/interface/src/entity/index.ts:
--------------------------------------------------------------------------------
1 | export * from './user'
2 | export * from './claims'
3 |
--------------------------------------------------------------------------------
/packages/interface/src/entity/user.ts:
--------------------------------------------------------------------------------
1 | import { Role } from './claims'
2 |
3 | export interface User {
4 | id: string
5 | role: Role
6 | disabled: boolean
7 | createdAt: number
8 | updatedAt: number
9 | }
10 |
--------------------------------------------------------------------------------
/packages/interface/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './entity'
2 | export * from './request'
3 |
--------------------------------------------------------------------------------
/packages/interface/src/request/auth.ts:
--------------------------------------------------------------------------------
1 | import { CreateRequestType } from './base'
2 | import { User } from '../entity'
3 |
4 | export type SignInRequest = CreateRequestType<
5 | null,
6 | null,
7 | {
8 | userID: string
9 | password: string
10 | },
11 | 'body'
12 | >
13 |
14 | export type SignInResponse = {
15 | user: User
16 | customToken: string
17 | }
18 |
--------------------------------------------------------------------------------
/packages/interface/src/request/base.ts:
--------------------------------------------------------------------------------
1 | type RequestKeys = 'param' | 'query' | 'body'
2 | interface BaseRequest {
3 | param: Param
4 | query: Query
5 | body: Body
6 | }
7 |
8 | export type CreateRequestType = Pick, K>
9 |
10 | export type ExtractPropertyType = T[P]
11 |
--------------------------------------------------------------------------------
/packages/interface/src/request/index.ts:
--------------------------------------------------------------------------------
1 | export * from './auth'
2 | export * from './base'
3 |
--------------------------------------------------------------------------------
/packages/interface/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "target": "es5",
5 | "lib": ["dom", "es2018"],
6 | "strict": true,
7 | "strictNullChecks": true,
8 | "noImplicitAny": true,
9 | "noImplicitReturns": true,
10 | "skipLibCheck": true,
11 | "declaration": true,
12 | "declarationMap": true,
13 | "pretty": true,
14 | "newLine": "lf",
15 | "esModuleInterop": true,
16 | "moduleResolution": "node"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/web-antd/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [
4 | ["styled-components", { "ssr": true }],
5 | [
6 | "import",
7 | {
8 | "libraryName": "antd",
9 | "style": true
10 | }
11 | ]
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/packages/web-antd/.env.tmpl:
--------------------------------------------------------------------------------
1 | GOOGLE_APPLICATION_CREDENTIALS="./firebase.admin.key.json"
2 | FIREBASE_CLIENT_API_KEY="AIzaSyBKu1YQwebiQTU2lkdEy70sB0QfIsample"
3 | API_ORIGIN="http://localhost:3003"
--------------------------------------------------------------------------------
/packages/web-antd/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/packages/web-antd/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@abyssparanoia/web-antd",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "private": true,
6 | "scripts": {
7 | "start:dev": "next -p 3007",
8 | "start:prod": "next start -p 3007",
9 | "build": "run-p -l build:*",
10 | "build:next": "next build",
11 | "lint": "eslint --fix -c ./.eslintrc.json './src/**/*.{ts,tsx}'"
12 | },
13 | "dependencies": {
14 | "@abyssparanoia/fixtures": "0.0.1",
15 | "@quentin-sommer/react-useragent": "3.1.1",
16 | "@zeit/next-css": "1.0.1",
17 | "@zeit/next-less": "1.0.1",
18 | "antd": "4.16.13",
19 | "axios": "0.24.0",
20 | "babel-plugin-import": "1.13.3",
21 | "core-js": "3.19.1",
22 | "date-fns": "2.25.0",
23 | "http2": "3.3.7",
24 | "less": "4.1.2",
25 | "less-vars-to-js": "1.3.0",
26 | "moment": "2.29.2",
27 | "next": "10.2.3",
28 | "next-offline": "5.0.5",
29 | "nookies": "2.5.2",
30 | "null-loader": "4.0.1",
31 | "path": "0.12.7",
32 | "query-string": "7.0.1",
33 | "react": "17.0.2",
34 | "react-dom": "17.0.2",
35 | "react-google-button": "0.7.2",
36 | "styled-components": "5.3.3",
37 | "swr": "0.5.7"
38 | },
39 | "devDependencies": {
40 | "@babel/core": "7.12.10",
41 | "@types/faker": "5.5.9",
42 | "@types/node": "14.17.33",
43 | "@types/react": "17.0.35",
44 | "@types/react-dom": "17.0.11",
45 | "@types/styled-components": "5.1.15",
46 | "@types/ua-parser-js": "0.7.36",
47 | "babel-loader": "8.1.0",
48 | "babel-plugin-styled-components": "1.13.3",
49 | "react-docgen-typescript-loader": "3.7.2"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/packages/web-antd/polyfills.js:
--------------------------------------------------------------------------------
1 | import 'core-js'
2 |
3 | console.log('Load your polyfills')
4 |
--------------------------------------------------------------------------------
/packages/web-antd/src/assets/antd-custom.less:
--------------------------------------------------------------------------------
1 | @primary-color: #f0414c;
2 | @layout-header-height: 51px;
3 | @layout-header-padding: 0px 0px;
4 | @layout-header-background: #f8f8f8;
5 | @layout-body-background: #ffffff;
6 | @layout-footer-padding: 0px;
7 | @border-radius-base: 2px;
8 | // radio button
9 | @radio-button-checked-bg: rgba(240, 65, 76, 0.1);
10 | @radio-button-color: #303034;
11 |
12 | // button
13 | @border-color-base: #f0414c;
14 | @btn-default-color: #f0414c;
15 | @btn-border-radius-base: 5px;
16 | @btn-disable-color: #ffffff;
17 | @btn-disable-bg: rgba(240, 65, 76, 0.5);
18 | @btn-disable-border: rgba(240, 65, 76, 0.1);
19 |
20 | // input
21 | @input-color: '#000000';
22 | @input-border-color: '#d9d9d9';
23 |
24 | // checkbox
25 |
26 | @checkbox-size: 24px;
27 | @font-family: Noto Sans JP, Roboto, sans-serif;
28 |
--------------------------------------------------------------------------------
/packages/web-antd/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Document, { Head, Main, NextScript, DocumentContext } from 'next/document'
3 | import { ServerStyleSheet } from 'styled-components'
4 |
5 | export default class extends Document {
6 | static getInitialProps = async (ctx: DocumentContext) => {
7 | const styledComponentsSheet = new ServerStyleSheet()
8 | const originalRenderPage = ctx.renderPage
9 |
10 | // main ページのcssをここでレンダリングさせるためのenhancer
11 | ctx.renderPage = () =>
12 | originalRenderPage({
13 | enhanceApp: App => props => ({
14 | ...styledComponentsSheet.collectStyles()
15 | })
16 | })
17 |
18 | const initialProps = await Document.getInitialProps(ctx)
19 |
20 | return {
21 | ...initialProps,
22 | styles: <>{styledComponentsSheet.getStyleElement()}>
23 | }
24 | }
25 |
26 | render() {
27 | return (
28 |
29 |
30 |
31 | {/* Use minimum-scale=1 to enable GPU rasterization */}
32 |
33 |
34 |
38 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | )
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/packages/web-antd/src/pages/about.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Router from 'next/router'
3 | import { Button } from 'antd'
4 |
5 | type InitialProps = {}
6 |
7 | type Props = {} & InitialProps
8 |
9 | const About = (_: Props) => {
10 | return (
11 |
12 |
13 |
14 | )
15 | }
16 |
17 | export default About
18 |
--------------------------------------------------------------------------------
/packages/web-antd/src/pages/api/validate.tsx:
--------------------------------------------------------------------------------
1 | import { adminAuth, FIREBASE_CLIENT_API_KEY } from '@abyssparanoia/firebase-admin'
2 | import { NextApiRequest, NextApiResponse } from 'next'
3 | import axios from 'axios'
4 | import * as convertKeys from 'convert-keys'
5 |
6 | interface IRefreshIDTokenRequest {
7 | refreshToken: string
8 | grantType: 'refresh_token'
9 | }
10 |
11 | interface IRefreshIDTokenResponse {
12 | idToken: string
13 | refreshToken: string
14 | userId: string
15 | tokenType: string
16 | expiresIn: string
17 | projectId: string
18 | }
19 |
20 | const refreshIDToken = async ({ refreshToken }: Pick) => {
21 | const param: IRefreshIDTokenRequest = { refreshToken, grantType: 'refresh_token' }
22 | const res = await axios.post(
23 | `https://securetoken.googleapis.com/v1/token?key=${FIREBASE_CLIENT_API_KEY}`,
24 | { ...convertKeys.toSnake(param) }
25 | )
26 | return res.data
27 | }
28 |
29 | export const config = {
30 | api: {
31 | bodyParser: true
32 | }
33 | }
34 |
35 | export default async (req: NextApiRequest, res: NextApiResponse) => {
36 | let { idToken, refreshToken } = req.body as { idToken?: string; refreshToken?: string }
37 | if (!idToken || !refreshToken) {
38 | return res.status(403).send({
39 | message: 'Auth token missing.'
40 | })
41 | }
42 | const result = { refreshToken, idToken }
43 | try {
44 | await adminAuth.verifyIdToken(idToken).catch(async err => {
45 | if (err.code === 'auth/id-token-expired') {
46 | const { refreshToken: newRefreshToken, idToken: newIdToken } = await refreshIDToken({
47 | refreshToken: refreshToken!
48 | })
49 | result.idToken = newIdToken
50 | result.refreshToken = newRefreshToken
51 | return
52 | }
53 | throw err
54 | })
55 | return res.status(200).send(result)
56 | } catch (err) {
57 | console.error(err)
58 | return res.status(401).send({
59 | message: err.message
60 | })
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/packages/web-antd/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ExNextPageContext } from 'next'
3 | import Router from 'next/router'
4 | import { authorize, useAuthCookie, useSignIn, useSignOut } from '@abyssparanoia/fixtures'
5 | import { Button } from 'antd'
6 |
7 | type InitialProps = {}
8 | type Props = {} & InitialProps
9 |
10 | const Index = (_: Props) => {
11 | const { idToken } = useAuthCookie()
12 | const { handleSignInWithGoogle } = useSignIn()
13 | const { handleSignOut } = useSignOut()
14 |
15 | return (
16 |
17 |
18 | {!idToken && }
19 | {idToken && }
20 |
21 | )
22 | }
23 |
24 | export const getServerSideProps = async (ctx: ExNextPageContext) => {
25 | await authorize(ctx)
26 | return { props: {} }
27 | }
28 |
29 | export default Index
30 |
--------------------------------------------------------------------------------
/packages/web-antd/src/pages/sign_in.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useSignIn } from '@abyssparanoia/fixtures'
3 | import { Button } from 'antd'
4 |
5 | type InitialProps = {}
6 |
7 | type Props = {} & InitialProps
8 |
9 | const SignIn = (_: Props) => {
10 | const { handleSignInWithGoogle } = useSignIn()
11 |
12 | return (
13 |
14 |
15 |
16 | )
17 | }
18 |
19 | export default SignIn
20 |
--------------------------------------------------------------------------------
/packages/web-antd/src/types/next.d.ts:
--------------------------------------------------------------------------------
1 | import Express from 'express'
2 | import { NextPageContext } from 'next'
3 | import { DocumentContext } from 'next/document'
4 |
5 | declare module 'next' {
6 | type ExNextPageContext = Omit & {
7 | req?: Express.Request
8 | res?: Express.Response
9 | }
10 | }
11 |
12 | declare module 'next/document' {
13 | type ExDocumentContext = DocumentContext & {
14 | req?: Express.Request
15 | res?: Express.Response
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/web-antd/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "removeComments": true,
5 | "esModuleInterop": true,
6 | "lib": ["dom", "es2017"],
7 | "target": "es6",
8 | "sourceMap": true,
9 | "rootDir": ".",
10 | "outDir": "./dist",
11 | "jsx": "preserve",
12 | "baseUrl": "./",
13 | "strict": true,
14 | "strictNullChecks": true,
15 | "strictPropertyInitialization": true,
16 | "noImplicitReturns": true,
17 | "noImplicitAny": true,
18 | "types": ["node"],
19 | "allowJs": false,
20 | "skipLibCheck": true,
21 | "forceConsistentCasingInFileNames": true,
22 | "moduleResolution": "node",
23 | "resolveJsonModule": true,
24 | "isolatedModules": true,
25 | "noEmit": true
26 | },
27 | "exclude": ["node_modules", "dist"],
28 | "include": ["next-env.d.ts", "src/types/*.d.ts", "**/*.ts", "**/*.tsx"]
29 | }
30 |
--------------------------------------------------------------------------------
/packages/web-graphql/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [["styled-components", { "ssr": true }]]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/web-graphql/.env.tmpl:
--------------------------------------------------------------------------------
1 | GOOGLE_APPLICATION_CREDENTIALS="./firebase.admin.key.json"
2 | FIREBASE_CLIENT_API_KEY="AIzaSyBKu1YQwebiQTU2lkdEy70sB0QfIsample"
3 | API_ORIGIN="http://localhost:3003"
--------------------------------------------------------------------------------
/packages/web-graphql/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/packages/web-graphql/next.config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 |
3 | const path = require('path')
4 | const Dotenv = require('dotenv-webpack')
5 |
6 | module.exports = {
7 | webpack: config => {
8 | config.plugins = config.plugins || []
9 |
10 | config.plugins = [
11 | ...config.plugins,
12 |
13 | new Dotenv({
14 | path: path.join(__dirname, '.env'),
15 | systemvars: true
16 | })
17 | ]
18 |
19 | config.resolve.alias['src'] = path.join(__dirname, 'src')
20 | config.resolve.alias['modules'] = path.join(__dirname, '/src/modules')
21 | config.resolve.alias['pages'] = path.join(__dirname, '/src/pages')
22 | config.resolve.alias['components'] = path.join(__dirname, '/src/components')
23 |
24 | return config
25 | },
26 | distDir: 'dist/src/.next'
27 | }
28 |
--------------------------------------------------------------------------------
/packages/web-graphql/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["next.config.js"],
3 | "exec": "ts-node -r dotenv/config --project ./src/tsconfig.server.json ./src/run.ts"
4 | }
5 |
--------------------------------------------------------------------------------
/packages/web-graphql/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@abyssparanoia/web-graphql",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "private": true,
6 | "scripts": {
7 | "start:dev": "nodemon --ext ts --watch src",
8 | "start:prod": "NODE_ENV=production node dist/src/run.js",
9 | "build": "run-p -l build:*",
10 | "build:next": "next build",
11 | "build:server": "tsc -b ./src/tsconfig.server.json",
12 | "lint": "eslint --fix -c ./.eslintrc.json './src/**/*.{ts,tsx}'"
13 | },
14 | "dependencies": {
15 | "@abyssparanoia/graphql": "0.0.1",
16 | "@material-ui/core": "4.12.3",
17 | "@material-ui/icons": "4.11.2",
18 | "@material-ui/lab": "4.0.0-alpha.60",
19 | "@material-ui/styles": "4.11.4",
20 | "@quentin-sommer/react-useragent": "3.1.1",
21 | "apollo-cache-inmemory": "1.6.6",
22 | "apollo-client": "2.6.10",
23 | "apollo-link": "1.2.14",
24 | "apollo-link-context": "1.0.20",
25 | "apollo-link-error": "1.1.13",
26 | "apollo-link-http": "1.5.17",
27 | "axios": "0.24.0",
28 | "body-parser": "1.19.0",
29 | "convert-keys": "1.3.4",
30 | "core-js": "3.19.1",
31 | "date-fns": "2.25.0",
32 | "dotenv": "10.0.0",
33 | "dotenv-webpack": "7.0.3",
34 | "faker": "5.5.3",
35 | "firebase": "7.24.0",
36 | "firebase-admin": "9.12.0",
37 | "formik": "2.2.9",
38 | "formik-material-ui": "3.0.1",
39 | "next": "10.2.3",
40 | "next-offline": "5.0.5",
41 | "next-with-apollo": "5.2.1",
42 | "nookies": "2.5.2",
43 | "path": "0.12.7",
44 | "query-string": "7.0.1",
45 | "react": "17.0.2",
46 | "react-apollo": "3.1.5",
47 | "react-dom": "17.0.2",
48 | "styled-components": "5.3.3",
49 | "swr": "0.5.7"
50 | },
51 | "devDependencies": {
52 | "@babel/core": "7.12.10",
53 | "@types/body-parser": "1.19.1",
54 | "@types/faker": "5.5.9",
55 | "@types/react": "17.0.35",
56 | "@types/react-dom": "17.0.11",
57 | "@types/styled-components": "5.1.15",
58 | "@types/ua-parser-js": "0.7.36",
59 | "babel-loader": "8.1.0",
60 | "babel-plugin-styled-components": "1.13.3",
61 | "react-docgen-typescript-loader": "3.7.2"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/components/thema.ts:
--------------------------------------------------------------------------------
1 | import { createMuiTheme } from '@material-ui/core/styles'
2 | import { red } from '@material-ui/core/colors'
3 |
4 | // sample theme
5 | const theme = createMuiTheme({
6 | palette: {
7 | primary: {
8 | light: '#757ce8',
9 | main: '#282c34',
10 | dark: '#002884',
11 | contrastText: '#fff'
12 | },
13 | secondary: {
14 | light: '#ff7961',
15 | main: '#f44336',
16 | dark: '#ba000d',
17 | contrastText: '#000'
18 | },
19 | error: {
20 | main: red.A400
21 | },
22 | background: {
23 | default: '#c9c9c9'
24 | }
25 | }
26 | })
27 |
28 | export default theme
29 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/factory.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response } from 'express'
2 | import next from 'next'
3 | import { adminAuth } from './firebase/admin'
4 | import * as bodyParser from 'body-parser'
5 | import cors from 'cors'
6 |
7 | const dev = process.env.NODE_ENV !== 'production'
8 |
9 | const app = next({ dir: '.', dev })
10 | const handle = app.getRequestHandler()
11 |
12 | export const appFactory = async () => {
13 | await app.prepare()
14 | const server = express()
15 |
16 | server.use(cors())
17 | server.use(bodyParser.json())
18 |
19 | server.use((req: Request, _: Response, next: Function) => {
20 | req.firebaseAuth = adminAuth
21 | next()
22 | })
23 |
24 | // nextjs routing
25 | server.get('*', (req, res) => handle(req, res))
26 | server.post('*', (req, res) => handle(req, res))
27 | server.put('*', (req, res) => handle(req, res))
28 | server.patch('*', (req, res) => handle(req, res))
29 | server.delete('*', (req, res) => handle(req, res))
30 |
31 | return server
32 | }
33 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/firebase/admin.ts:
--------------------------------------------------------------------------------
1 | import * as admin from 'firebase-admin'
2 |
3 | const app = process.env.GCLOUD_PROJECT
4 | ? admin.initializeApp()
5 | : admin.initializeApp({
6 | credential: admin.credential.applicationDefault()
7 | })
8 |
9 | const adminAuth = app.auth()
10 |
11 | export { app, adminAuth }
12 |
13 | declare global {
14 | namespace Express {
15 | interface Request {
16 | firebaseAuth: admin.auth.Auth
17 | firebaseStore: admin.firestore.Firestore
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/firebase/client.ts:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase/app'
2 | import 'firebase/auth'
3 |
4 | if (!firebase.apps.length) {
5 | firebase.initializeApp(require('../../firebase.client.key.json'))
6 | }
7 |
8 | const auth = firebase.auth()
9 |
10 | class FirebaseAuthenticationError extends Error {
11 | constructor(error: firebase.auth.Error) {
12 | super(error.message)
13 | this.name = new.target.name
14 | Object.setPrototypeOf(this, new.target.prototype)
15 |
16 | // https://github.com/firebase/firebase-js-sdk/blob/master/packages/auth/src/error_auth.js
17 | switch (error.code) {
18 | case 'auth/email-already-in-use':
19 | this.message = '入力されたメールアドレスはすでに使用されています。'
20 | break
21 | case 'auth/invalid-email':
22 | this.message = '不正なメールアドレスです'
23 | break
24 | case 'auth/user-not-found':
25 | this.message = 'ユーザーが見つかりませんでした'
26 | break
27 | case 'auth/wrong-password':
28 | this.message = 'パスワードが一致しません'
29 | break
30 | default:
31 | this.message = 'エラーが発生しました'
32 | break
33 | }
34 | }
35 | }
36 |
37 | export { firebase, auth, FirebaseAuthenticationError }
38 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/fixtures/auth/cookie.ts:
--------------------------------------------------------------------------------
1 | import { parseCookies, setCookie, destroyCookie } from 'nookies'
2 | import { NextPageContext } from 'next'
3 |
4 | export const getTokenFromCookie = (ctx?: NextPageContext): { idToken?: string; refreshToken?: string } => {
5 | const { idToken, refreshToken } = parseCookies(ctx)
6 | return { idToken, refreshToken }
7 | }
8 |
9 | export const setTokenToCookie = (
10 | { idToken, refreshToken }: { idToken?: string; refreshToken?: string },
11 | ctx?: NextPageContext
12 | ): void => {
13 | idToken &&
14 | setCookie(ctx, 'idToken', idToken, {
15 | maxAge: 30 * 24 * 60 * 60,
16 | path: '/'
17 | })
18 | refreshToken &&
19 | setCookie(ctx, 'refreshToken', refreshToken, {
20 | maxAge: 30 * 24 * 60 * 60,
21 | path: '/'
22 | })
23 | }
24 |
25 | export const removeTokenFromCookie = (ctx?: NextPageContext): void => {
26 | destroyCookie(ctx, 'idToken')
27 | destroyCookie(ctx, 'refreshToken')
28 | }
29 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/fixtures/auth/swr-cache-path.ts:
--------------------------------------------------------------------------------
1 | export const SWRCachePath = {
2 | AUTH_COOKIE: `/auth/cookie`
3 | } as const
4 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/fixtures/utility/hooks.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, DependencyList } from 'react'
2 |
3 | export function useEffectAsync(effect: () => void, deps?: DependencyList): void {
4 | useEffect(() => {
5 | effect()
6 | // eslint-disable-next-line react-hooks/exhaustive-deps
7 | }, deps)
8 | }
9 |
10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
11 | export type UnwrapFunc = T extends (...arg: any) => Promise ? U : T
12 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/fixtures/utility/index.ts:
--------------------------------------------------------------------------------
1 | export * from './hooks'
2 | export * from './ssr'
3 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/fixtures/utility/ssr.ts:
--------------------------------------------------------------------------------
1 | export const isBrowser = () => typeof window !== 'undefined'
2 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import App, { AppInitialProps } from 'next/app'
3 | import { WithApolloProps } from 'next-with-apollo'
4 | import { StylesProvider } from '@material-ui/styles'
5 | import { MuiThemeProvider } from '@material-ui/core/styles'
6 | import CssBaseline from '@material-ui/core/CssBaseline'
7 | import { ThemeProvider as StyledThemeProvider } from 'styled-components'
8 | import Head from 'next/head'
9 | import theme from 'src/components/thema'
10 | import withApollo from 'src/fixtures/withApollo'
11 |
12 | class NextApp extends App> {
13 | componentDidCatch = (error: Error, errorInfo: React.ErrorInfo) => {
14 | super.componentDidCatch(error, errorInfo)
15 | }
16 |
17 | render() {
18 | const { Component, pageProps, apollo } = this.props
19 |
20 | return (
21 | <>
22 |
23 | boiler
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | >
34 | )
35 | }
36 | }
37 |
38 | export default withApollo(NextApp)
39 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Document, { Head, Main, NextScript, DocumentContext } from 'next/document'
3 | import { ServerStyleSheets } from '@material-ui/styles'
4 | import { ServerStyleSheet } from 'styled-components'
5 | import theme from 'src/components/thema'
6 |
7 | export default class extends Document {
8 | static getInitialProps = async (ctx: DocumentContext) => {
9 | const materialSheets = new ServerStyleSheets()
10 | const styledComponentsSheet = new ServerStyleSheet()
11 | const originalRenderPage = ctx.renderPage
12 |
13 | // main ページのcssをここでレンダリングさせるためのenhancer
14 | ctx.renderPage = () =>
15 | originalRenderPage({
16 | enhanceApp: App => props => ({
17 | ...styledComponentsSheet.collectStyles(),
18 | ...materialSheets.collect()
19 | })
20 | })
21 |
22 | const initialProps = await Document.getInitialProps(ctx)
23 |
24 | return {
25 | ...initialProps,
26 | styles: (
27 | <>
28 | {materialSheets.getStyleElement()}
29 | {styledComponentsSheet.getStyleElement()}
30 | >
31 | )
32 | }
33 | }
34 |
35 | render() {
36 | return (
37 |
38 |
39 |
40 | {/* Use minimum-scale=1 to enable GPU rasterization */}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/pages/about.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button } from '@material-ui/core'
3 | import { useListUsersQuery } from '@abyssparanoia/graphql'
4 | import Router from 'next/router'
5 |
6 | type InitialProps = {}
7 |
8 | type Props = {} & InitialProps
9 |
10 | const About = (_: Props) => {
11 | const { data, loading } = useListUsersQuery({})
12 |
13 | console.log(loading)
14 | console.log(data)
15 |
16 | return (
17 |
18 |
19 | {loading &&
loading.......
}
20 | {data && (
21 |
22 | {data.list.map(user => (
23 |
{user.name}
24 | ))}
25 |
26 | )}
27 |
28 | )
29 | }
30 |
31 | export default About
32 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ExNextPageContext } from 'next'
3 | import { Button } from '@material-ui/core'
4 | import { useListUsersQuery } from '@abyssparanoia/graphql'
5 | import Router from 'next/router'
6 | import { authorize } from 'src/fixtures/auth/middleware'
7 | import { useAuthCookie, useSignIn, useSignOut } from 'src/fixtures/auth/hooks'
8 |
9 | type InitialProps = {}
10 | type Props = {} & InitialProps
11 |
12 | const Index = (_: Props) => {
13 | const { data, loading } = useListUsersQuery({})
14 | const { idToken } = useAuthCookie()
15 | const { handleSignInWithGoogle } = useSignIn()
16 | const { handleSignOut } = useSignOut()
17 |
18 | return (
19 |
20 |
21 | {loading &&
loading.......
}
22 | {data && (
23 |
24 | {data.list.map(user => (
25 |
{user.name}
26 | ))}
27 |
28 | )}
29 | {!idToken &&
}
30 | {idToken &&
}
31 |
32 | )
33 | }
34 |
35 | Index.getInitialProps = async (ctx: ExNextPageContext) => {
36 | await authorize(ctx)
37 | return {}
38 | }
39 |
40 | export default Index
41 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/pages/sign_in.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button } from '@material-ui/core'
3 | import { useSignIn } from 'src/fixtures/auth/hooks'
4 |
5 | type InitialProps = {}
6 |
7 | type Props = {} & InitialProps
8 |
9 | const SignIn = (_: Props) => {
10 | const { handleSignInWithGoogle } = useSignIn()
11 |
12 | return (
13 |
14 |
15 |
16 | )
17 | }
18 |
19 | export default SignIn
20 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/run.ts:
--------------------------------------------------------------------------------
1 | import { appFactory } from './factory'
2 |
3 | const PORT = process.env.PORT || 3005
4 |
5 | const run = async () => {
6 | try {
7 | const server = await appFactory()
8 | server.listen(PORT, () => {
9 | console.log(`> Ready on http://localhost:${PORT}`)
10 | })
11 | } catch (err) {
12 | console.error(err)
13 | }
14 | }
15 |
16 | run()
17 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/static/img/icon192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abyssparanoia/nextjs-firebase-boilerplate/fa5c6a0967ea7801341df1e7babd2da2574330c2/packages/web-graphql/src/static/img/icon192.png
--------------------------------------------------------------------------------
/packages/web-graphql/src/static/img/icon512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abyssparanoia/nextjs-firebase-boilerplate/fa5c6a0967ea7801341df1e7babd2da2574330c2/packages/web-graphql/src/static/img/icon512.png
--------------------------------------------------------------------------------
/packages/web-graphql/src/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "KSR-SPA",
3 | "short_name": "KSR-SPA",
4 | "start_url": "/",
5 | "display": "standalone",
6 | "background_color": "#fff",
7 | "description": "Next.js SPA Bolier Plate.",
8 | "icons": [
9 | {
10 | "src": "img/icon192.png",
11 | "sizes": "192x192",
12 | "type": "image/png"
13 | },
14 | {
15 | "src": "img/icon512.png",
16 | "sizes": "512x512",
17 | "type": "image/png"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2017",
5 | "outDir": "../dist",
6 | "declaration": false,
7 | "module": "commonjs",
8 | "noEmit": false
9 | },
10 | "include": ["."],
11 | "exclude": ["./pages", "./modules", "./firebase/client.ts", "./components", "./static"]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/web-graphql/src/types/next.d.ts:
--------------------------------------------------------------------------------
1 | import Express from 'express'
2 | import { NextPageContext } from 'next'
3 | import { DocumentContext } from 'next/document'
4 | import { Store } from 'redux'
5 | import { ReduxStore } from 'src/modules/reducer'
6 |
7 | declare module 'next' {
8 | type ExNextPageContext = Omit & {
9 | req?: Express.Request
10 | res?: Express.Response
11 | store: Store
12 | }
13 | }
14 |
15 | declare module 'next/document' {
16 | type ExDocumentContext = DocumentContext & {
17 | req?: Express.Request
18 | res?: Express.Response
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/web-graphql/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "removeComments": true,
5 | "esModuleInterop": true,
6 | "lib": ["dom", "es2017"],
7 | "target": "es6",
8 | "sourceMap": true,
9 | "rootDir": ".",
10 | "outDir": "./dist",
11 | "jsx": "preserve",
12 | "baseUrl": "./",
13 | "strict": true,
14 | "strictNullChecks": true,
15 | "strictPropertyInitialization": true,
16 | "noImplicitReturns": true,
17 | "noImplicitAny": true,
18 | "types": ["node"],
19 | "allowJs": false,
20 | "skipLibCheck": true,
21 | "forceConsistentCasingInFileNames": true,
22 | "moduleResolution": "node",
23 | "resolveJsonModule": true,
24 | "isolatedModules": true,
25 | "noEmit": true
26 | },
27 | "exclude": ["node_modules", "dist"],
28 | "include": ["next-env.d.ts", "src/types/*.d.ts", "**/*.ts", "**/*.tsx"]
29 | }
30 |
--------------------------------------------------------------------------------
/packages/web-swr/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [["styled-components", { "ssr": true }]]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/web-swr/.env.tmpl:
--------------------------------------------------------------------------------
1 | GOOGLE_APPLICATION_CREDENTIALS="./firebase.admin.key.json"
2 | FIREBASE_CLIENT_API_KEY="AIzaSyBKu1YQwebiQTU2lkdEy70sB0QfIsample"
3 | API_ORIGIN="http://localhost:3003"
--------------------------------------------------------------------------------
/packages/web-swr/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/packages/web-swr/next.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = {
4 | webpack: config => {
5 | config.plugins = config.plugins || []
6 |
7 | config.resolve.alias['src'] = path.join(__dirname, 'src')
8 | config.resolve.alias['modules'] = path.join(__dirname, '/src/modules')
9 | config.resolve.alias['pages'] = path.join(__dirname, '/src/pages')
10 | config.resolve.alias['components'] = path.join(__dirname, '/src/components')
11 |
12 | const splitChunks = config.optimization && config.optimization.splitChunks
13 | if (splitChunks) {
14 | const cacheGroups = splitChunks.cacheGroups
15 | const preactModules = /[\\/]node_modules[\\/](preact|preact-render-to-string|preact-context-provider)[\\/]/
16 | if (cacheGroups.framework) {
17 | cacheGroups.preact = Object.assign({}, cacheGroups.framework, {
18 | test: preactModules
19 | })
20 | cacheGroups.commons.name = 'framework'
21 | } else {
22 | cacheGroups.preact = {
23 | name: 'commons',
24 | chunks: 'all',
25 | test: preactModules
26 | }
27 | }
28 | }
29 |
30 | const originalEntry = config.entry
31 | config.entry = async () => {
32 | const entries = await originalEntry()
33 | if (entries['main.js'] && !entries['main.js'].includes('./polyfills.js')) {
34 | entries['main.js'].unshift('./polyfills.js')
35 | }
36 | return entries
37 | }
38 |
39 | return config
40 | },
41 | distDir: 'dist/src/.next'
42 | }
43 |
--------------------------------------------------------------------------------
/packages/web-swr/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@abyssparanoia/web-swr",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "private": true,
6 | "scripts": {
7 | "start:dev": "next -p 3006",
8 | "start:prod": "next start -p 3006",
9 | "build": "run-p -l build:*",
10 | "build:next": "next build",
11 | "lint": "eslint --fix -c ./.eslintrc.json './src/**/*.{ts,tsx}'"
12 | },
13 | "dependencies": {
14 | "@abyssparanoia/fixtures": "0.0.1",
15 | "@quentin-sommer/react-useragent": "3.1.1",
16 | "axios": "0.24.0",
17 | "core-js": "3.19.1",
18 | "date-fns": "2.25.0",
19 | "faker": "5.5.3",
20 | "firebase": "7.24.0",
21 | "firebase-admin": "9.12.0",
22 | "moment": "2.29.2",
23 | "next": "10.2.3",
24 | "next-offline": "5.0.5",
25 | "nookies": "2.5.2",
26 | "path": "0.12.7",
27 | "query-string": "7.0.1",
28 | "react": "17.0.2",
29 | "react-dom": "17.0.2",
30 | "react-google-button": "0.7.2",
31 | "styled-components": "5.3.3",
32 | "swr": "0.5.7"
33 | },
34 | "devDependencies": {
35 | "@babel/core": "7.12.10",
36 | "@types/faker": "5.5.9",
37 | "@types/node": "14.17.33",
38 | "@types/react": "17.0.35",
39 | "@types/react-dom": "17.0.11",
40 | "@types/styled-components": "5.1.15",
41 | "@types/ua-parser-js": "0.7.36",
42 | "babel-loader": "8.1.0",
43 | "babel-plugin-styled-components": "1.13.3",
44 | "react-docgen-typescript-loader": "3.7.2"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/packages/web-swr/polyfills.js:
--------------------------------------------------------------------------------
1 | import 'core-js'
2 |
3 | console.log('Load your polyfills')
4 |
--------------------------------------------------------------------------------
/packages/web-swr/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Document, { Head, Main, NextScript, DocumentContext } from 'next/document'
3 | import { ServerStyleSheet } from 'styled-components'
4 |
5 | export default class extends Document {
6 | static getInitialProps = async (ctx: DocumentContext) => {
7 | const styledComponentsSheet = new ServerStyleSheet()
8 | const originalRenderPage = ctx.renderPage
9 |
10 | // main ページのcssをここでレンダリングさせるためのenhancer
11 | ctx.renderPage = () =>
12 | originalRenderPage({
13 | enhanceApp: App => props => ({
14 | ...styledComponentsSheet.collectStyles()
15 | })
16 | })
17 |
18 | const initialProps = await Document.getInitialProps(ctx)
19 |
20 | return {
21 | ...initialProps,
22 | styles: <>{styledComponentsSheet.getStyleElement()}>
23 | }
24 | }
25 |
26 | render() {
27 | return (
28 |
29 |
30 |
31 | {/* Use minimum-scale=1 to enable GPU rasterization */}
32 |
33 |
34 |
38 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | )
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/packages/web-swr/src/pages/about.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Router from 'next/router'
3 |
4 | type InitialProps = {}
5 |
6 | type Props = {} & InitialProps
7 |
8 | const About = (_: Props) => {
9 | return (
10 |
11 |
12 |
13 | )
14 | }
15 |
16 | export default About
17 |
--------------------------------------------------------------------------------
/packages/web-swr/src/pages/api/validate.tsx:
--------------------------------------------------------------------------------
1 | import { adminAuth, FIREBASE_CLIENT_API_KEY } from '@abyssparanoia/firebase-admin'
2 | import { NextApiRequest, NextApiResponse } from 'next'
3 | import axios from 'axios'
4 | import * as convertKeys from 'convert-keys'
5 |
6 | interface IRefreshIDTokenRequest {
7 | refreshToken: string
8 | grantType: 'refresh_token'
9 | }
10 |
11 | interface IRefreshIDTokenResponse {
12 | idToken: string
13 | refreshToken: string
14 | userId: string
15 | tokenType: string
16 | expiresIn: string
17 | projectId: string
18 | }
19 |
20 | const refreshIDToken = async ({ refreshToken }: Pick) => {
21 | const param: IRefreshIDTokenRequest = { refreshToken, grantType: 'refresh_token' }
22 | const res = await axios.post(
23 | `https://securetoken.googleapis.com/v1/token?key=${FIREBASE_CLIENT_API_KEY}`,
24 | { ...convertKeys.toSnake(param) }
25 | )
26 | return res.data
27 | }
28 |
29 | export const config = {
30 | api: {
31 | bodyParser: true
32 | }
33 | }
34 |
35 | export default async (req: NextApiRequest, res: NextApiResponse) => {
36 | let { idToken, refreshToken } = req.body as { idToken?: string; refreshToken?: string }
37 | if (!idToken || !refreshToken) {
38 | return res.status(403).send({
39 | message: 'Auth token missing.'
40 | })
41 | }
42 | const result = { refreshToken, idToken }
43 | try {
44 | await adminAuth.verifyIdToken(idToken).catch(async err => {
45 | if (err.code === 'auth/id-token-expired') {
46 | const { refreshToken: newRefreshToken, idToken: newIdToken } = await refreshIDToken({
47 | refreshToken: refreshToken!
48 | })
49 | result.idToken = newIdToken
50 | result.refreshToken = newRefreshToken
51 | return
52 | }
53 | throw err
54 | })
55 | return res.status(200).send(result)
56 | } catch (err) {
57 | console.error(err)
58 | return res.status(401).send({
59 | message: err.message
60 | })
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/packages/web-swr/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ExNextPageContext } from 'next'
3 | import Router from 'next/router'
4 | import { authorize } from '@abyssparanoia/fixtures'
5 | import { useAuthCookie, useSignIn, useSignOut } from '@abyssparanoia/fixtures'
6 |
7 | type InitialProps = {}
8 | type Props = {} & InitialProps
9 |
10 | const Index = (_: Props) => {
11 | const { idToken } = useAuthCookie()
12 | const { handleSignInWithGoogle } = useSignIn()
13 | const { handleSignOut } = useSignOut()
14 |
15 | return (
16 |
17 |
18 | {!idToken && }
19 | {idToken && }
20 |
21 | )
22 | }
23 |
24 | export const getServerSideProps = async (ctx: ExNextPageContext) => {
25 | await authorize(ctx)
26 | return { props: {} }
27 | }
28 |
29 | export default Index
30 |
--------------------------------------------------------------------------------
/packages/web-swr/src/pages/sign_in.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useSignIn } from '@abyssparanoia/fixtures'
3 |
4 | type InitialProps = {}
5 |
6 | type Props = {} & InitialProps
7 |
8 | const SignIn = (_: Props) => {
9 | const { handleSignInWithGoogle } = useSignIn()
10 |
11 | return (
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default SignIn
19 |
--------------------------------------------------------------------------------
/packages/web-swr/src/types/next.d.ts:
--------------------------------------------------------------------------------
1 | import Express from 'express'
2 | import { NextPageContext } from 'next'
3 | import { DocumentContext } from 'next/document'
4 |
5 | declare module 'next' {
6 | type ExNextPageContext = Omit & {
7 | req?: Express.Request
8 | res?: Express.Response
9 | }
10 | }
11 |
12 | declare module 'next/document' {
13 | type ExDocumentContext = DocumentContext & {
14 | req?: Express.Request
15 | res?: Express.Response
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/web-swr/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "removeComments": true,
5 | "esModuleInterop": true,
6 | "lib": ["dom", "es2017"],
7 | "target": "es6",
8 | "sourceMap": true,
9 | "rootDir": ".",
10 | "outDir": "./dist",
11 | "jsx": "preserve",
12 | "baseUrl": "./",
13 | "strict": true,
14 | "strictNullChecks": true,
15 | "strictPropertyInitialization": true,
16 | "noImplicitReturns": true,
17 | "noImplicitAny": true,
18 | "types": ["node"],
19 | "allowJs": false,
20 | "skipLibCheck": true,
21 | "forceConsistentCasingInFileNames": true,
22 | "moduleResolution": "node",
23 | "resolveJsonModule": true,
24 | "isolatedModules": true,
25 | "noEmit": true
26 | },
27 | "exclude": ["node_modules", "dist"],
28 | "include": ["next-env.d.ts", "src/types/*.d.ts", "**/*.ts", "**/*.tsx"]
29 | }
30 |
--------------------------------------------------------------------------------
/packages/web/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": [["styled-components", { "ssr": true }]]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/web/.env.tmpl:
--------------------------------------------------------------------------------
1 | GOOGLE_APPLICATION_CREDENTIALS="./firebase.admin.key.json"
2 | FIREBASE_CLIENT_API_KEY="AIzaSyBKu1YQwebiQTU2lkdEy70sB0QfIsample"
--------------------------------------------------------------------------------
/packages/web/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine
2 |
3 | RUN mkdir -p /app
4 | ADD . /app
5 | WORKDIR /app
6 |
7 | RUN yarn
8 | RUN yarn build
9 |
10 |
11 | ENV PORT 3000
12 | EXPOSE 3000
13 |
14 | ENTRYPOINT ["yarn", "workspace", "@abyssparanoia/web","start:prod"]
--------------------------------------------------------------------------------
/packages/web/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/packages/web/next.config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config()
2 |
3 | const path = require('path')
4 | const Dotenv = require('dotenv-webpack')
5 |
6 | module.exports = {
7 | webpack: config => {
8 | config.plugins = config.plugins || []
9 |
10 | config.plugins = [
11 | ...config.plugins,
12 |
13 | new Dotenv({
14 | path: path.join(__dirname, '.env'),
15 | systemvars: true
16 | })
17 | ]
18 |
19 | config.resolve.alias['src'] = path.join(__dirname, 'src')
20 | config.resolve.alias['modules'] = path.join(__dirname, '/src/modules')
21 | config.resolve.alias['pages'] = path.join(__dirname, '/src/pages')
22 | config.resolve.alias['components'] = path.join(__dirname, '/src/components')
23 |
24 | const splitChunks = config.optimization && config.optimization.splitChunks
25 | if (splitChunks) {
26 | const cacheGroups = splitChunks.cacheGroups
27 | const preactModules = /[\\/]node_modules[\\/](preact|preact-render-to-string|preact-context-provider)[\\/]/
28 | if (cacheGroups.framework) {
29 | cacheGroups.preact = Object.assign({}, cacheGroups.framework, {
30 | test: preactModules
31 | })
32 | cacheGroups.commons.name = 'framework'
33 | } else {
34 | cacheGroups.preact = {
35 | name: 'commons',
36 | chunks: 'all',
37 | test: preactModules
38 | }
39 | }
40 | }
41 |
42 | return config
43 | },
44 | distDir: 'dist/src/.next'
45 | }
46 |
--------------------------------------------------------------------------------
/packages/web/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["next.config.js"],
3 | "exec": "ts-node -r dotenv/config --project ./src/tsconfig.server.json ./src/run.ts"
4 | }
5 |
--------------------------------------------------------------------------------
/packages/web/src/components/atoms/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abyssparanoia/nextjs-firebase-boilerplate/fa5c6a0967ea7801341df1e7babd2da2574330c2/packages/web/src/components/atoms/.gitkeep
--------------------------------------------------------------------------------
/packages/web/src/components/atoms/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { makeStyles } from '@material-ui/core/styles'
3 | import { default as OriginalButton, ButtonProps } from '@material-ui/core/Button'
4 | import { Theme } from '@material-ui/core'
5 |
6 | const useStyles = makeStyles((theme: Theme) => ({
7 | button: {
8 | margin: theme.spacing(1)
9 | }
10 | }))
11 |
12 | interface Props extends ButtonProps {}
13 |
14 | export const Button = (props: Props) => {
15 | const classes = useStyles({})
16 | return
17 | }
18 |
--------------------------------------------------------------------------------
/packages/web/src/components/atoms/CircularIndeterminate.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
3 | import { default as MuiCircularProgress, CircularProgressProps } from '@material-ui/core/CircularProgress'
4 |
5 | interface ICircularProgressProps extends CircularProgressProps {}
6 |
7 | const useStyles = makeStyles((theme: Theme) =>
8 | createStyles({
9 | progress: {
10 | margin: theme.spacing(2)
11 | }
12 | })
13 | )
14 |
15 | export const CircularIndeterminate = (props: ICircularProgressProps): JSX.Element => {
16 | const classes = useStyles()
17 | return
18 | }
19 |
--------------------------------------------------------------------------------
/packages/web/src/components/atoms/Link.tsx:
--------------------------------------------------------------------------------
1 | import React, { FunctionComponent } from 'react'
2 | import { default as OriginalLink, LinkProps } from 'next/link'
3 |
4 | interface Props extends LinkProps {}
5 |
6 | export const Link: FunctionComponent = ({ children, ...props }) => (
7 |
8 | {children}
9 |
10 | )
11 |
--------------------------------------------------------------------------------
/packages/web/src/components/atoms/SnackbarContent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { default as MuiSnackbarContent, SnackbarContentProps } from '@material-ui/core/SnackbarContent'
3 | import clsx from 'clsx'
4 | import IconButton from '@material-ui/core/IconButton'
5 | import { makeStyles, Theme } from '@material-ui/core/styles'
6 | import { amber, green } from '@material-ui/core/colors'
7 | import CheckCircleIcon from '@material-ui/icons/CheckCircle'
8 | import CloseIcon from '@material-ui/icons/Close'
9 | import ErrorIcon from '@material-ui/icons/Error'
10 | import InfoIcon from '@material-ui/icons/Info'
11 | import WarningIcon from '@material-ui/icons/Warning'
12 |
13 | const variantIcon = {
14 | success: CheckCircleIcon,
15 | warning: WarningIcon,
16 | error: ErrorIcon,
17 | info: InfoIcon
18 | }
19 |
20 | const useStyles = makeStyles((theme: Theme) => ({
21 | success: {
22 | backgroundColor: green[600]
23 | },
24 | error: {
25 | backgroundColor: theme.palette.error.dark
26 | },
27 | info: {
28 | backgroundColor: theme.palette.primary.main
29 | },
30 | warning: {
31 | backgroundColor: amber[700]
32 | },
33 | icon: {
34 | fontSize: 20
35 | },
36 | iconVariant: {
37 | opacity: 0.9,
38 | marginRight: theme.spacing(1)
39 | },
40 | message: {
41 | display: 'flex',
42 | alignItems: 'center'
43 | }
44 | }))
45 |
46 | interface IProps extends Omit {
47 | message: string
48 | variant: keyof typeof variantIcon
49 | onClose: () => void
50 | }
51 |
52 | export const SnackbarContent = (props: IProps) => {
53 | const classes = useStyles()
54 | const { message, onClose, variant, ...other } = props
55 | const Icon = variantIcon[variant]
56 |
57 | return (
58 |
63 |
64 | {message}
65 |
66 | }
67 | action={[
68 |
69 |
70 |
71 | ]}
72 | {...other}
73 | />
74 | )
75 | }
76 |
--------------------------------------------------------------------------------
/packages/web/src/components/atoms/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Button'
2 | export * from './Link'
3 |
--------------------------------------------------------------------------------
/packages/web/src/components/global/Feedback.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 | import { Snackbar } from 'src/components/moleclues/Snackbar'
3 | import { ReduxStore } from 'src/modules/reducer'
4 | import { popFeedback } from 'src/modules/feedback'
5 | import { useDispatch, useSelector } from 'react-redux'
6 |
7 | interface Props {}
8 |
9 | export const GlobalFeedback = (_: Props) => {
10 | const { feedbackList } = useSelector(({ feedback: { list } }: ReduxStore) => ({
11 | feedbackList: list
12 | }))
13 |
14 | const dispatch = useDispatch()
15 | const handlePopFeedback = useCallback((id: string) => () => dispatch(popFeedback({ id })), [dispatch])
16 |
17 | return (
18 | <>
19 | {feedbackList.map(({ id, variant, message }) => (
20 |
21 | ))}
22 | >
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/packages/web/src/components/global/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Head from 'next/head'
3 | import { MenuAppBar } from './AppBar'
4 | import { GlobalFeedback } from './Feedback'
5 |
6 | type Props = {
7 | title?: string
8 | }
9 |
10 | export const Global: React.FunctionComponent = ({ children, title = 'This is the default title' }) => {
11 | return (
12 |
13 |
14 |
{title}
15 |
16 |
17 |
18 |
19 |
20 | {children}
21 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/packages/web/src/components/moleclues/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abyssparanoia/nextjs-firebase-boilerplate/fa5c6a0967ea7801341df1e7babd2da2574330c2/packages/web/src/components/moleclues/.gitkeep
--------------------------------------------------------------------------------
/packages/web/src/components/moleclues/Progress.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { CircularIndeterminate } from 'src/components/atoms/CircularIndeterminate'
4 |
5 | const ProgressWrapper = styled.div`
6 | left: 0;
7 | top: 0;
8 | width: 100%;
9 | height: 100%;
10 | position: fixed;
11 | background: #222;
12 | opacity: 0.7;
13 | z-index: 100;
14 | `
15 | const ProgressInner = styled.div`
16 | left: 50%;
17 | position: absolute;
18 | top: 50%;
19 | transform: translate(-50%, -50%);
20 | `
21 |
22 | export const Progress = (): JSX.Element => {
23 | return (
24 |
25 |
26 |
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/packages/web/src/components/moleclues/Snackbar.tsx:
--------------------------------------------------------------------------------
1 | import React, { SyntheticEvent, useState } from 'react'
2 | import CheckCircleIcon from '@material-ui/icons/CheckCircle'
3 | import ErrorIcon from '@material-ui/icons/Error'
4 | import InfoIcon from '@material-ui/icons/Info'
5 | import { default as MuiSnackbar, SnackbarProps } from '@material-ui/core/Snackbar'
6 | import WarningIcon from '@material-ui/icons/Warning'
7 | import { SnackbarContent } from 'src/components/atoms/SnackbarContent'
8 |
9 | const variantIcon = {
10 | success: CheckCircleIcon,
11 | warning: WarningIcon,
12 | error: ErrorIcon,
13 | info: InfoIcon
14 | }
15 |
16 | interface IProps extends Omit {
17 | message: string
18 | variant: keyof typeof variantIcon
19 | open?: boolean
20 | onClose?: () => void
21 | }
22 |
23 | export const Snackbar = ({ message, variant, onClose, open }: IProps) => {
24 | const [isOpen, setIsOpen] = useState(open || true)
25 |
26 | const handleClose = (event?: SyntheticEvent, reason?: string) => {
27 | if (reason === 'clickaway') {
28 | return
29 | }
30 | if (onClose) onClose()
31 | setIsOpen(false)
32 | }
33 |
34 | return (
35 |
44 |
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/packages/web/src/components/organisms/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abyssparanoia/nextjs-firebase-boilerplate/fa5c6a0967ea7801341df1e7babd2da2574330c2/packages/web/src/components/organisms/.gitkeep
--------------------------------------------------------------------------------
/packages/web/src/components/theme.ts:
--------------------------------------------------------------------------------
1 | import { createMuiTheme } from '@material-ui/core/styles'
2 | import { red } from '@material-ui/core/colors'
3 |
4 | // sample theme
5 | const theme = createMuiTheme({
6 | palette: {
7 | primary: {
8 | light: '#757ce8',
9 | main: '#282c34',
10 | dark: '#002884',
11 | contrastText: '#fff'
12 | },
13 | secondary: {
14 | light: '#ff7961',
15 | main: '#f44336',
16 | dark: '#ba000d',
17 | contrastText: '#000'
18 | },
19 | error: {
20 | main: red.A400
21 | },
22 | background: {
23 | default: '#c9c9c9'
24 | }
25 | }
26 | })
27 |
28 | export default theme
29 |
--------------------------------------------------------------------------------
/packages/web/src/factory.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response } from 'express'
2 | import next from 'next'
3 | import { auth, db } from './firebase/admin'
4 | import * as bodyParser from 'body-parser'
5 | import cors from 'cors'
6 |
7 | const dev = process.env.NODE_ENV !== 'production'
8 |
9 | const app = next({ dir: '.', dev })
10 | const handle = app.getRequestHandler()
11 |
12 | export const appFactory = async () => {
13 | await app.prepare()
14 | const server = express()
15 |
16 | server.use(cors())
17 | server.use(bodyParser.json())
18 |
19 | server.use((req: Request, _: Response, next: Function) => {
20 | req.firebaseAuth = auth
21 | req.firebaseStore = db
22 | next()
23 | })
24 |
25 | // nextjs routing
26 | server.get('*', (req, res) => handle(req, res))
27 | server.post('*', (req, res) => handle(req, res))
28 | server.put('*', (req, res) => handle(req, res))
29 | server.patch('*', (req, res) => handle(req, res))
30 | server.delete('*', (req, res) => handle(req, res))
31 |
32 | return server
33 | }
34 |
--------------------------------------------------------------------------------
/packages/web/src/firebase/admin.ts:
--------------------------------------------------------------------------------
1 | import * as admin from 'firebase-admin'
2 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
3 |
4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
5 | import { Credential } from './interface'
6 |
7 | const app = process.env.GCLOUD_PROJECT
8 | ? admin.initializeApp()
9 | : admin.initializeApp({
10 | credential: admin.credential.applicationDefault()
11 | })
12 |
13 | const auth = app.auth()
14 | const db = app.firestore()
15 |
16 | export { auth, db }
17 |
18 | export default app
19 |
20 | declare global {
21 | namespace Express {
22 | interface Request {
23 | firebaseAuth: admin.auth.Auth
24 | firebaseStore: admin.firestore.Firestore
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/web/src/firebase/client.ts:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase/app'
2 | import 'firebase/auth'
3 |
4 | if (!firebase.apps.length) {
5 | firebase.initializeApp(require('../../firebase.client.key.json'))
6 | }
7 |
8 | const auth = firebase.auth()
9 |
10 | class FirebaseAuthenticationError extends Error {
11 | constructor(error: firebase.auth.Error) {
12 | super(error.message)
13 | this.name = new.target.name
14 | Object.setPrototypeOf(this, new.target.prototype)
15 |
16 | // https://github.com/firebase/firebase-js-sdk/blob/master/packages/auth/src/error_auth.js
17 | switch (error.code) {
18 | case 'auth/email-already-in-use':
19 | this.message = '入力されたメールアドレスはすでに使用されています。'
20 | break
21 | case 'auth/invalid-email':
22 | this.message = '不正なメールアドレスです'
23 | break
24 | case 'auth/user-not-found':
25 | this.message = 'ユーザーが見つかりませんでした'
26 | break
27 | case 'auth/wrong-password':
28 | this.message = 'パスワードが一致しません'
29 | break
30 | default:
31 | this.message = 'エラーが発生しました'
32 | break
33 | }
34 | }
35 | }
36 |
37 | export { firebase, auth, FirebaseAuthenticationError }
38 |
--------------------------------------------------------------------------------
/packages/web/src/firebase/interface.ts:
--------------------------------------------------------------------------------
1 | export interface Credential {
2 | uid: string
3 | accessToken: string
4 | refreshToken: string
5 | }
6 |
--------------------------------------------------------------------------------
/packages/web/src/index.ts:
--------------------------------------------------------------------------------
1 | import { appFactory } from './factory'
2 | import * as functions from 'firebase-functions'
3 |
4 | export const nextApp = functions.https.onRequest(async (req, res) => {
5 | const server = await appFactory()
6 | server(req as any, res as any)
7 | })
8 |
--------------------------------------------------------------------------------
/packages/web/src/modules/entities/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abyssparanoia/nextjs-firebase-boilerplate/fa5c6a0967ea7801341df1e7babd2da2574330c2/packages/web/src/modules/entities/.gitkeep
--------------------------------------------------------------------------------
/packages/web/src/modules/feedback.ts:
--------------------------------------------------------------------------------
1 | import actionCreatorFactory from 'typescript-fsa'
2 | import { reducerWithInitialState } from 'typescript-fsa-reducers'
3 |
4 | const actionCreator = actionCreatorFactory('feedback')
5 |
6 | export interface Feedback {
7 | id: string
8 | variant: 'success' | 'warning' | 'error' | 'info'
9 | message: string
10 | }
11 |
12 | const actions = {
13 | pushFeedback: actionCreator>('PUSH_FEEDBACK'),
14 | popFeedback: actionCreator<{ id: string }>('POP_FEEDBACK')
15 | }
16 |
17 | export interface State {
18 | list: Feedback[]
19 | }
20 |
21 | const initialState: State = {
22 | list: []
23 | }
24 |
25 | export const pushFeedback = actions.pushFeedback
26 |
27 | export const popFeedback = actions.popFeedback
28 |
29 | export const reducer = reducerWithInitialState(initialState)
30 | .case(actions.pushFeedback, (state, payload) => {
31 | const id = Math.random().toString(36).substring(2, 15)
32 |
33 | return {
34 | ...state,
35 | list: [{ id, ...payload }, ...state.list]
36 | }
37 | })
38 | .case(actions.popFeedback, (state, payload) => ({
39 | ...state,
40 | list: state.list.filter(feedback => feedback.id != payload.id)
41 | }))
42 | .build()
43 |
--------------------------------------------------------------------------------
/packages/web/src/modules/reducer.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import { State as TableState, reducer as tableReducer } from './table'
3 | import { State as AuthState, reducer as authReducer } from './auth'
4 | import { State as FeedbackState, reducer as feedbackReducer } from './feedback'
5 |
6 | export interface ReduxStore {
7 | table: TableState
8 | auth: AuthState
9 | feedback: FeedbackState
10 | }
11 |
12 | export const createRootReducer = () =>
13 | combineReducers({
14 | table: tableReducer,
15 | auth: authReducer,
16 | feedback: feedbackReducer
17 | })
18 |
--------------------------------------------------------------------------------
/packages/web/src/modules/repositories/index.ts:
--------------------------------------------------------------------------------
1 | export * from './httpClient'
2 | export * from './auth'
3 | export * from './sample'
4 |
--------------------------------------------------------------------------------
/packages/web/src/modules/repositories/sample.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from './httpClient'
2 | import { SignInRequest } from '@abyssparanoia/interface'
3 |
4 | export const samplePost = async () => {
5 | const req: SignInRequest['body'] = {
6 | userID: 'userID',
7 | password: 'password'
8 | }
9 | const res = await new HttpClient({ url: `http://localhost:3001` }).post(req)
10 | console.log(res.data)
11 | }
12 |
--------------------------------------------------------------------------------
/packages/web/src/modules/services/auth.ts:
--------------------------------------------------------------------------------
1 | import { ExNextPageContext } from 'next'
2 | import { stringify } from 'query-string'
3 | import { Credential } from 'src/firebase/interface'
4 | import Router from 'next/router'
5 | import { refreshIDToken, setCredentialToCookie, getCredentialFromCookie } from 'src/modules/repositories/auth'
6 | import { actions } from 'src/modules/auth'
7 |
8 | export const authenticate = async (ctx: ExNextPageContext): Promise => {
9 | const { req, store } = ctx
10 |
11 | const credential = getCredentialFromCookie(ctx)
12 |
13 | // on server
14 | if (req && credential) {
15 | await req.firebaseAuth.verifyIdToken(credential.accessToken).catch(async err => {
16 | if (err.code === 'auth/id-token-expired') {
17 | const { refreshToken, idToken: accessToken, userId: uid } = await refreshIDToken({
18 | refreshToken: credential.refreshToken!
19 | })
20 | setCredentialToCookie({ refreshToken, accessToken, uid }, ctx)
21 | credential.accessToken = accessToken
22 | credential.refreshToken = refreshToken
23 | return
24 | }
25 | throw err
26 | })
27 | }
28 |
29 | await store.dispatch(actions.setCredential(credential))
30 |
31 | return credential
32 | }
33 |
34 | export const authorize = async ({ req, res, store }: ExNextPageContext) => {
35 | const credential = store.getState().auth.credential
36 |
37 | // on server
38 | if (req && res && !credential) {
39 | const redirectTo = req.url
40 |
41 | res!.writeHead(302, {
42 | Location: `/sign_in?${stringify({ redirectTo })}`
43 | })
44 | res.end()
45 | return
46 | }
47 |
48 | // on browser
49 | if (!res && !credential) {
50 | const redirectTo = Router.pathname
51 | Router.push(`/sign_in?${stringify({ redirectTo })}`)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/packages/web/src/modules/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './auth'
2 | export * from './routing'
3 |
--------------------------------------------------------------------------------
/packages/web/src/modules/services/routing.ts:
--------------------------------------------------------------------------------
1 | import Router from 'next/router'
2 | import { ExNextPageContext } from 'next'
3 |
4 | export const redirect = ({ req, res }: ExNextPageContext, url: string) => {
5 | if (req && res) {
6 | res.redirect(url)
7 | res.end()
8 | return
9 | }
10 |
11 | Router.push(url)
12 | }
13 |
--------------------------------------------------------------------------------
/packages/web/src/modules/table.ts:
--------------------------------------------------------------------------------
1 | import actionCreatorFactory from 'typescript-fsa'
2 | import { reducerWithInitialState } from 'typescript-fsa-reducers'
3 | import { Dispatch } from 'redux'
4 |
5 | const timeout = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
6 |
7 | const actionCreator = actionCreatorFactory('table')
8 |
9 | export const actions = {
10 | fetchTableList: actionCreator.async('list')
11 | }
12 |
13 | export interface State {
14 | list: string[]
15 | isLoading: boolean
16 | error?: Error
17 | }
18 |
19 | const initialState: State = {
20 | list: [],
21 | isLoading: false
22 | }
23 |
24 | export const fetchTableList = () => async (dispatch: Dispatch) => {
25 | dispatch(actions.fetchTableList.started())
26 | await timeout(1500)
27 | // mock data
28 | const list = ['users', 'posts', 'post_favorites']
29 | return dispatch(actions.fetchTableList.done({ result: { list } }))
30 | }
31 |
32 | export const reducer = reducerWithInitialState(initialState)
33 | .case(actions.fetchTableList.started, state => ({
34 | ...state,
35 | isLoading: true
36 | }))
37 | .case(actions.fetchTableList.done, (state, payload) => ({ ...state, isLoading: false, list: payload.result.list }))
38 | .case(actions.fetchTableList.failed, (state, payload) => ({ ...state, isLoading: false, error: payload.error }))
39 | .build()
40 |
--------------------------------------------------------------------------------
/packages/web/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import App, { AppProps as NextAppProps, AppContext } from 'next/app'
3 | import { StylesProvider } from '@material-ui/styles'
4 | import { MuiThemeProvider } from '@material-ui/core/styles'
5 | import CssBaseline from '@material-ui/core/CssBaseline'
6 | import { ThemeProvider as StyledThemeProvider } from 'styled-components'
7 | import Head from 'next/head'
8 | import theme from 'src/components/theme'
9 | import withRedux from 'next-redux-wrapper'
10 | import { makeStore } from 'src/store'
11 | import { Provider } from 'react-redux'
12 | import { Store } from 'redux'
13 | import { ReduxStore } from 'src/modules/reducer'
14 | import { Global } from 'src/components/global'
15 | import { ExNextPageContext } from 'next'
16 | import { authenticate } from 'src/modules/services'
17 |
18 | interface AppProps extends NextAppProps {
19 | ua: string
20 | store: Store
21 | }
22 |
23 | class NextApp extends App {
24 | static async getInitialProps({ Component, ctx }: AppContext) {
25 | let pageProps = {}
26 |
27 | const { store } = ctx as ExNextPageContext
28 | if (!store.getState().auth.credential) {
29 | await authenticate(ctx as ExNextPageContext)
30 | }
31 |
32 | if (Component.getInitialProps) {
33 | pageProps = await Component.getInitialProps(ctx)
34 | }
35 |
36 | return {
37 | pageProps
38 | }
39 | }
40 |
41 | componentDidCatch = (error: Error, errorInfo: React.ErrorInfo) => {
42 | super.componentDidCatch(error, errorInfo)
43 | }
44 |
45 | render() {
46 | const { Component, pageProps, store } = this.props
47 |
48 | return (
49 | <>
50 |
51 | boiler
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | >
66 | )
67 | }
68 | }
69 |
70 | export default withRedux(makeStore)(NextApp)
71 |
--------------------------------------------------------------------------------
/packages/web/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Document, { Head, Main, NextScript, DocumentContext } from 'next/document'
3 | import { ServerStyleSheets } from '@material-ui/styles'
4 | import { ServerStyleSheet } from 'styled-components'
5 | import theme from 'src/components/theme'
6 |
7 | export default class extends Document {
8 | static getInitialProps = async (ctx: DocumentContext) => {
9 | const materialSheets = new ServerStyleSheets()
10 | const styledComponentsSheet = new ServerStyleSheet()
11 | const originalRenderPage = ctx.renderPage
12 |
13 | // main ページのcssをここでレンダリングさせるためのenhancer
14 | ctx.renderPage = () =>
15 | originalRenderPage({
16 | enhanceApp: App => props => ({
17 | ...styledComponentsSheet.collectStyles(),
18 | ...materialSheets.collect()
19 | })
20 | })
21 |
22 | const initialProps = await Document.getInitialProps(ctx)
23 |
24 | return {
25 | ...initialProps,
26 | styles: (
27 | <>
28 | {materialSheets.getStyleElement()}
29 | {styledComponentsSheet.getStyleElement()}
30 | >
31 | )
32 | }
33 | }
34 |
35 | render() {
36 | return (
37 |
38 |
39 |
40 | {/* Use minimum-scale=1 to enable GPU rasterization */}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | )
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/packages/web/src/pages/about.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ExNextPageContext } from 'next'
3 | import { authorize } from 'src/modules/services'
4 |
5 | type Props = {}
6 |
7 | const About = (_: Props) => {
8 | return About page
9 | }
10 |
11 | About.getInitialProps = async (ctx: ExNextPageContext): Promise => {
12 | await authorize(ctx)
13 | }
14 |
15 | export default About
16 |
--------------------------------------------------------------------------------
/packages/web/src/pages/elb-health-check.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from 'next/dynamic'
2 | import React, { Fragment } from 'react'
3 |
4 | const HealthCheck = () => {
5 | return ok
6 | }
7 |
8 | export default dynamic(() => Promise.resolve(HealthCheck), {
9 | ssr: false
10 | })
11 |
--------------------------------------------------------------------------------
/packages/web/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react'
2 | import { ExNextPageContext } from 'next'
3 | import { fetchTableList } from 'src/modules/table'
4 | import { pushFeedback } from 'src/modules/feedback'
5 | import { useSelector, useDispatch } from 'react-redux'
6 | import { ReduxStore } from 'src/modules/reducer'
7 | import { Button } from '@material-ui/core'
8 |
9 | type InitialProps = {}
10 |
11 | type Props = {} & InitialProps
12 |
13 | const Index = (_: Props) => {
14 | const tableList = useSelector((state: ReduxStore) => state.table.list)
15 | const dispatch = useDispatch()
16 |
17 | const handleClick = useCallback(() => dispatch(pushFeedback({ variant: 'info', message: 'hello world!' })), [
18 | dispatch
19 | ])
20 |
21 | console.log(tableList)
22 |
23 | return (
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | Index.getInitialProps = async ({ store }: ExNextPageContext): Promise => {
31 | // sample dispatch
32 | await store.dispatch(fetchTableList() as any)
33 |
34 | return {}
35 | }
36 |
37 | export default Index
38 |
--------------------------------------------------------------------------------
/packages/web/src/pages/login_required.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ExNextPageContext } from 'next'
3 | import Link from 'next/link'
4 | import { authorize } from 'src/modules/services'
5 |
6 | type Props = {}
7 |
8 | const LoginRequired = (_: Props) => {
9 | return (
10 | <>
11 | ログイン済みユーザーのみが見れる
12 |
13 | 初期レンダリング後の認証情報に関して、AuthContextを使うかfirebase authのSDKのcurrentUserを使うかは要相談
14 |
15 |
16 | トップページへ
17 |
18 | >
19 | )
20 | }
21 |
22 | LoginRequired.getInitialProps = async (ctx: ExNextPageContext): Promise => {
23 | await authorize(ctx)
24 | }
25 |
26 | export default LoginRequired
27 |
--------------------------------------------------------------------------------
/packages/web/src/run.ts:
--------------------------------------------------------------------------------
1 | import { appFactory } from './factory'
2 |
3 | const PORT = process.env.PORT || 3000
4 |
5 | const run = async () => {
6 | try {
7 | const server = await appFactory()
8 | server.listen(PORT, () => {
9 | console.log(`> Ready on http://localhost:${PORT}`)
10 | })
11 | } catch (err) {
12 | console.error(err)
13 | }
14 | }
15 |
16 | run()
17 |
--------------------------------------------------------------------------------
/packages/web/src/static/img/icon192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abyssparanoia/nextjs-firebase-boilerplate/fa5c6a0967ea7801341df1e7babd2da2574330c2/packages/web/src/static/img/icon192.png
--------------------------------------------------------------------------------
/packages/web/src/static/img/icon512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abyssparanoia/nextjs-firebase-boilerplate/fa5c6a0967ea7801341df1e7babd2da2574330c2/packages/web/src/static/img/icon512.png
--------------------------------------------------------------------------------
/packages/web/src/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "KSR-SPA",
3 | "short_name": "KSR-SPA",
4 | "start_url": "/",
5 | "display": "standalone",
6 | "background_color": "#fff",
7 | "description": "Next.js SPA Bolier Plate.",
8 | "icons": [
9 | {
10 | "src": "img/icon192.png",
11 | "sizes": "192x192",
12 | "type": "image/png"
13 | },
14 | {
15 | "src": "img/icon512.png",
16 | "sizes": "512x512",
17 | "type": "image/png"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/web/src/store.ts:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux'
2 | import { createRootReducer, ReduxStore } from './modules/reducer'
3 | import { composeWithDevTools } from 'redux-devtools-extension'
4 | import { logger } from 'redux-logger'
5 | import thunk from 'redux-thunk'
6 | import { MakeStore, MakeStoreOptions } from 'next-redux-wrapper'
7 |
8 | export const makeStore: MakeStore = (initialState: ReduxStore, _options: MakeStoreOptions) =>
9 | createStore(
10 | createRootReducer(),
11 | initialState,
12 | process.env.NODE_ENV !== 'production' ? composeWithDevTools(applyMiddleware(thunk, logger)) : applyMiddleware(thunk)
13 | )
14 |
--------------------------------------------------------------------------------
/packages/web/src/tsconfig.server.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es2017",
5 | "outDir": "../dist",
6 | "declaration": false,
7 | "module": "commonjs",
8 | "noEmit": false
9 | },
10 | "include": ["."],
11 | "exclude": ["./pages", "./modules", "./firebase/client.ts", "./components", "./static"]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/web/src/types/next.d.ts:
--------------------------------------------------------------------------------
1 | import Express from 'express'
2 | import { NextPageContext } from 'next'
3 | import { DocumentContext } from 'next/document'
4 | import { Store } from 'redux'
5 | import { ReduxStore } from 'src/modules/reducer'
6 |
7 | declare module 'next' {
8 | type ExNextPageContext = Omit & {
9 | req?: Express.Request
10 | res?: Express.Response
11 | store: Store
12 | }
13 | }
14 |
15 | declare module 'next/document' {
16 | type ExDocumentContext = DocumentContext & {
17 | req?: Express.Request
18 | res?: Express.Response
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/web/thema.ts:
--------------------------------------------------------------------------------
1 | import { createMuiTheme } from '@material-ui/core/styles'
2 | import { red } from '@material-ui/core/colors'
3 |
4 | // sample theme
5 | const theme = createMuiTheme({
6 | palette: {
7 | primary: {
8 | light: '#757ce8',
9 | main: '#282c34',
10 | dark: '#002884',
11 | contrastText: '#fff'
12 | },
13 | secondary: {
14 | light: '#ff7961',
15 | main: '#f44336',
16 | dark: '#ba000d',
17 | contrastText: '#000'
18 | },
19 | error: {
20 | main: red.A400
21 | },
22 | background: {
23 | default: '#c9c9c9'
24 | }
25 | }
26 | })
27 |
28 | export default theme
29 |
--------------------------------------------------------------------------------
/packages/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "removeComments": true,
5 | "esModuleInterop": true,
6 | "lib": ["dom", "es2017"],
7 | "target": "es6",
8 | "sourceMap": true,
9 | "rootDir": ".",
10 | "outDir": "./dist",
11 | "jsx": "preserve",
12 | "baseUrl": "./",
13 | "strict": true,
14 | "strictNullChecks": true,
15 | "strictPropertyInitialization": true,
16 | "noImplicitReturns": true,
17 | "noImplicitAny": true,
18 | "types": ["node"],
19 | "allowJs": false,
20 | "skipLibCheck": true,
21 | "forceConsistentCasingInFileNames": true,
22 | "moduleResolution": "node",
23 | "resolveJsonModule": true,
24 | "isolatedModules": true,
25 | "noEmit": true
26 | },
27 | "exclude": ["node_modules", "dist"],
28 | "include": ["next-env.d.ts", "src/types/*.d.ts", "**/*.ts", "**/*.tsx"]
29 | }
30 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "ignoreDeps": [
4 | "electron",
5 | "electron-builder",
6 | "electron-updater",
7 | "electron-load-devtool",
8 | "electron-log",
9 | "@types/electron-load-devtool"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "removeComments": true,
5 | "esModuleInterop": true,
6 | "lib": ["dom", "es2017"],
7 | "target": "es6",
8 | "sourceMap": true,
9 | "rootDir": ".",
10 | "outDir": "./dist",
11 | "jsx": "preserve",
12 | "baseUrl": "./",
13 | "strict": true,
14 | "strictNullChecks": true,
15 | "strictPropertyInitialization": true,
16 | "noImplicitReturns": true,
17 | "noImplicitAny": true,
18 | "types": ["node"],
19 | "allowJs": true,
20 | "skipLibCheck": true,
21 | "forceConsistentCasingInFileNames": true,
22 | "noEmit": true,
23 | "moduleResolution": "node",
24 | "resolveJsonModule": true,
25 | "isolatedModules": true,
26 | "emitDecoratorMetadata": true,
27 | "experimentalDecorators": true
28 | },
29 | "exclude": ["node_modules", "dist"],
30 | "include": ["next-env.d.ts", "src/types/*.d.ts", "**/*.ts", "**/*.tsx"]
31 | }
32 |
--------------------------------------------------------------------------------