├── backend ├── .envrc ├── .dockerignore ├── pkg │ ├── utils │ │ ├── uuid.go │ │ ├── slice.go │ │ ├── url.go │ │ └── base64.go │ ├── static │ │ └── fonts │ │ │ └── Archivo_Black │ │ │ ├── ArchivoBlack-Regular.ttf │ │ │ └── OFL.txt │ ├── handlers │ │ ├── api │ │ │ ├── local │ │ │ │ └── main.go │ │ │ └── lambda │ │ │ │ └── main.go │ │ └── deletelgtm │ │ │ └── main.go │ ├── entities │ │ ├── image.go │ │ ├── report.go │ │ └── lgtm.go │ ├── controllers │ │ ├── health_controller.go │ │ ├── errcode.go │ │ ├── images_controller.go │ │ ├── renderer.go │ │ ├── cors_middleware.go │ │ ├── logger_middleware.go │ │ ├── reports_controller.go │ │ ├── error_response_logger_middleware.go │ │ └── lgtms_controller.go │ ├── infrastructures │ │ ├── dynamodb │ │ │ └── dynamodb.go │ │ ├── imagesearch │ │ │ └── imagesearch.go │ │ ├── s3 │ │ │ └── s3.go │ │ ├── router │ │ │ └── router.go │ │ └── lgtmgen │ │ │ └── lgtmgen.go │ └── repositories │ │ ├── reports_repository.go │ │ └── lgtms_repository.go ├── .env.template ├── package.json ├── .gitignore ├── .air.toml ├── docker-compose.yml ├── containers │ └── app │ │ └── Dockerfile ├── go.mod └── serverless.yml ├── .codeclimate.yml ├── README.md ├── frontend ├── .prettierignore ├── src │ ├── models │ │ ├── lgtm.ts │ │ ├── image.ts │ │ └── report.ts │ ├── components │ │ ├── App │ │ │ ├── index.tsx │ │ │ └── App.tsx │ │ ├── Layout │ │ │ ├── index.tsx │ │ │ ├── Layout.tsx │ │ │ ├── Footer.tsx │ │ │ ├── theme.ts │ │ │ └── Header.tsx │ │ ├── pages │ │ │ ├── Home │ │ │ │ ├── index.tsx │ │ │ │ ├── Home.tsx │ │ │ │ ├── FavoritesPanel.tsx │ │ │ │ ├── Tabs.tsx │ │ │ │ ├── UploadButton.tsx │ │ │ │ ├── SearchImagesPanel.tsx │ │ │ │ └── LgtmsPanel.tsx │ │ │ ├── NotFound │ │ │ │ ├── index.tsx │ │ │ │ └── NotFound.tsx │ │ │ ├── Precautions │ │ │ │ ├── index.tsx │ │ │ │ └── Precautions.tsx │ │ │ └── PrivacyPolicy │ │ │ │ ├── index.tsx │ │ │ │ └── PrivacyPolicy.tsx │ │ ├── utils │ │ │ ├── Field.tsx │ │ │ ├── Form.tsx │ │ │ ├── LoadableButton.tsx │ │ │ ├── Link.tsx │ │ │ ├── Loading.tsx │ │ │ ├── Modal.tsx │ │ │ ├── ModalCard.tsx │ │ │ └── Meta.tsx │ │ ├── model │ │ │ ├── lgtm │ │ │ │ ├── LgtmCardList.tsx │ │ │ │ ├── LgtmCard.tsx │ │ │ │ ├── LgtmForm.tsx │ │ │ │ └── LgtmCardButtonGroup.tsx │ │ │ ├── image │ │ │ │ ├── ImageCardList.tsx │ │ │ │ └── ImageCard.tsx │ │ │ └── report │ │ │ │ └── ReportForm.tsx │ │ └── providers │ │ │ └── ToastProvider.tsx │ ├── lib │ │ ├── errors.ts │ │ ├── dataUrl.ts │ │ ├── emotion.ts │ │ ├── dataStorage.ts │ │ ├── apiClient.ts │ │ └── imageFileReader.ts │ ├── routes.ts │ ├── global.d.ts │ ├── recoil │ │ └── atoms.ts │ ├── hooks │ │ ├── i18n.ts │ │ ├── translateHooks.ts │ │ ├── imageHooks.ts │ │ ├── reportHooks.ts │ │ └── lgtmHooks.ts │ └── locales │ │ ├── translate.ts │ │ ├── ja.tsx │ │ └── en.tsx ├── pages │ ├── index.tsx │ ├── 404.tsx │ ├── _app.tsx │ ├── precautions.tsx │ ├── privacy.tsx │ └── _document.tsx ├── public │ ├── card.png │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ └── manifest.json ├── .prettierrc ├── next.config.js ├── .env.template ├── next-sitemap.config.js ├── next-env.d.ts ├── tsconfig.json ├── .gitignore ├── package.json └── .eslintrc.js ├── terraform ├── cicd │ ├── locals.tf │ ├── provider.tf │ ├── terraform.tf │ ├── .terraform.lock.hcl │ └── iam.tf ├── app │ ├── modules │ │ └── aws │ │ │ ├── variables.tf │ │ │ ├── terraform.tf │ │ │ ├── locals.tf │ │ │ ├── ecr.tf │ │ │ ├── apigateway.tf │ │ │ ├── acm.tf │ │ │ ├── s3.tf │ │ │ ├── cloudfront.tf │ │ │ └── route53.tf │ ├── main.tf │ ├── provider.tf │ ├── tfsec.yml │ ├── terraform.tf │ ├── .tflint.hcl │ └── .terraform.lock.hcl └── .gitignore ├── e2e ├── .gitignore ├── cypress │ ├── fixtures │ │ └── images │ │ │ └── gray.png │ ├── support │ │ ├── e2e.ts │ │ └── commands.ts │ └── e2e │ │ └── frontend │ │ └── pages │ │ └── home.cy.ts ├── cypress.local.config.ts ├── cypress.dev.config.ts ├── package.json └── tsconfig.json ├── docs ├── architecture.png └── development.md ├── renovate.json ├── .github └── workflows │ ├── release_prod.yml │ ├── release_dev.yml │ ├── _e2e.yml │ ├── _release.yml │ └── _build.yml └── LICENSE /backend/.envrc: -------------------------------------------------------------------------------- 1 | dotenv 2 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | exclude_patterns: 2 | - "backend/mocks/" 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Moved to https://github.com/koki-develop/lgtmgen 2 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | /.serverless/ 2 | /node_modules/ 3 | /secrets.yml 4 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | /README.md 2 | /.next/ 3 | /terraform/.terraform/ 4 | -------------------------------------------------------------------------------- /frontend/src/models/lgtm.ts: -------------------------------------------------------------------------------- 1 | export type Lgtm = { 2 | id: string; 3 | }; 4 | -------------------------------------------------------------------------------- /terraform/cicd/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | prefix = "lgtm-generator-cicd" 3 | } 4 | -------------------------------------------------------------------------------- /e2e/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | 3 | /cypress/screenshots/ 4 | /cypress/videos/ 5 | -------------------------------------------------------------------------------- /terraform/app/modules/aws/variables.tf: -------------------------------------------------------------------------------- 1 | variable "stage" { 2 | type = string 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/components/App/index.tsx: -------------------------------------------------------------------------------- 1 | import App from './App'; 2 | 3 | export default App; 4 | -------------------------------------------------------------------------------- /frontend/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Home from '@/components/pages/Home'; 2 | 3 | export default Home; 4 | -------------------------------------------------------------------------------- /frontend/src/models/image.ts: -------------------------------------------------------------------------------- 1 | export type Image = { 2 | title: string; 3 | url: string; 4 | }; 5 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koki-develop/lgtm-generator/HEAD/docs/architecture.png -------------------------------------------------------------------------------- /frontend/src/components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import Layout from './Layout'; 2 | 3 | export default Layout; 4 | -------------------------------------------------------------------------------- /frontend/src/components/pages/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import Home from './Home'; 2 | 3 | export default Home; 4 | -------------------------------------------------------------------------------- /frontend/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import NotFound from '@/components/pages/NotFound'; 2 | 3 | export default NotFound; 4 | -------------------------------------------------------------------------------- /frontend/public/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koki-develop/lgtm-generator/HEAD/frontend/public/card.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koki-develop/lgtm-generator/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koki-develop/lgtm-generator/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koki-develop/lgtm-generator/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /terraform/app/main.tf: -------------------------------------------------------------------------------- 1 | module "aws" { 2 | source = "./modules/aws" 3 | 4 | stage = terraform.workspace 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/components/pages/NotFound/index.tsx: -------------------------------------------------------------------------------- 1 | import NotFound from './NotFound'; 2 | 3 | export default NotFound; 4 | -------------------------------------------------------------------------------- /frontend/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import App from '@/components/App'; 2 | import '@fontsource/archivo-black'; 3 | 4 | export default App; 5 | -------------------------------------------------------------------------------- /frontend/pages/precautions.tsx: -------------------------------------------------------------------------------- 1 | import Precautions from '@/components/pages/Precautions'; 2 | 3 | export default Precautions; 4 | -------------------------------------------------------------------------------- /frontend/pages/privacy.tsx: -------------------------------------------------------------------------------- 1 | import PrivacyPolicy from '@/components/pages/PrivacyPolicy'; 2 | 3 | export default PrivacyPolicy; 4 | -------------------------------------------------------------------------------- /frontend/src/components/pages/Precautions/index.tsx: -------------------------------------------------------------------------------- 1 | import Precautions from './Precautions'; 2 | 3 | export default Precautions; 4 | -------------------------------------------------------------------------------- /e2e/cypress/fixtures/images/gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koki-develop/lgtm-generator/HEAD/e2e/cypress/fixtures/images/gray.png -------------------------------------------------------------------------------- /frontend/src/components/pages/PrivacyPolicy/index.tsx: -------------------------------------------------------------------------------- 1 | import PrivacyPolicy from './PrivacyPolicy'; 2 | 3 | export default PrivacyPolicy; 4 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "trailingComma": "all", 5 | "arrowParens": "avoid" 6 | } 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>koki-develop/renovate-config"] 4 | } 5 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | i18n: { 3 | locales: ['ja', 'en'], 4 | defaultLocale: 'ja', 5 | }, 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /backend/pkg/utils/uuid.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | func UUIDV4() string { 8 | return uuid.New().String() 9 | } 10 | -------------------------------------------------------------------------------- /terraform/app/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-east-1" 3 | 4 | default_tags { 5 | tags = { 6 | App = "lgtm-generator" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/.env.template: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_STAGE=local 2 | NEXT_PUBLIC_API_ORIGIN=http://localhost:8080 3 | NEXT_PUBLIC_LGTMS_ORIGIN=http://localhost:9000/lgtm-generator-backend-local-images 4 | -------------------------------------------------------------------------------- /terraform/cicd/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-east-1" 3 | 4 | default_tags { 5 | tags = { 6 | App = "lgtm-generator" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/pkg/static/fonts/Archivo_Black/ArchivoBlack-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koki-develop/lgtm-generator/HEAD/backend/pkg/static/fonts/Archivo_Black/ArchivoBlack-Regular.ttf -------------------------------------------------------------------------------- /frontend/next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next-sitemap').IConfig} */ 2 | 3 | module.exports = { 4 | siteUrl: 'https://www.lgtmgen.org', 5 | generateRobotsTxt: true, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/src/lib/errors.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from 'ts-custom-error'; 2 | 3 | export class UnsupportedImageFormatError extends CustomError {} 4 | export class FileTooLargeError extends CustomError {} 5 | -------------------------------------------------------------------------------- /e2e/cypress.local.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | defaultCommandTimeout: 30000, 5 | e2e: { 6 | baseUrl: "http://localhost:3000", 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/src/routes.ts: -------------------------------------------------------------------------------- 1 | export const Routes = { 2 | home: '/', 3 | precautions: '/precautions', 4 | privacyPolicy: '/privacy', 5 | } as const; 6 | 7 | export type Routes = typeof Routes[keyof typeof Routes]; 8 | -------------------------------------------------------------------------------- /frontend/src/lib/dataUrl.ts: -------------------------------------------------------------------------------- 1 | export class DataUrl { 2 | constructor(private dataUrl: string) {} 3 | 4 | public toBase64(): string { 5 | return this.dataUrl.slice(this.dataUrl.indexOf(',') + 1); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /backend/.env.template: -------------------------------------------------------------------------------- 1 | STAGE=local 2 | ALLOW_ORIGIN=http://localhost:3000 3 | IMAGES_BASE_URL=http://localhost:9000/lgtm-generator-backend-local-images 4 | SLACK_API_TOKEN= 5 | GOOGLE_API_KEY= 6 | GOOGLE_CUSTOM_SEARCH_ENGINE_ID= 7 | -------------------------------------------------------------------------------- /frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /terraform/app/modules/aws/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.2.9" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 4.67.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /e2e/cypress.dev.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | defaultCommandTimeout: 30000, 5 | e2e: { 6 | baseUrl: "https://lgtm-generator-git-develop-koki-develop.vercel.app", 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /backend/pkg/utils/slice.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | func Shuffle[T any](s []T) { 9 | rand.Seed(time.Now().UnixNano()) 10 | rand.Shuffle(len(s), func(i, j int) { s[i], s[j] = s[j], s[i] }) 11 | } 12 | -------------------------------------------------------------------------------- /backend/pkg/handlers/api/local/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/koki-develop/lgtm-generator/backend/pkg/infrastructures/router" 4 | 5 | func main() { 6 | r := router.New() 7 | if err := r.Run(); err != nil { 8 | panic(err) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | readonly NEXT_PUBLIC_STAGE: 'local' | 'dev' | 'prod'; 4 | readonly NEXT_PUBLIC_GA_MEASUREMENT_ID: string; 5 | readonly NEXT_PUBLIC_API_ORIGIN: string; 6 | readonly NEXT_PUBLIC_LGTMS_ORIGIN: string; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /terraform/cicd/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "s3" { 3 | region = "us-east-1" 4 | bucket = "lgtm-generator-tfstates" 5 | key = "cicd/terraform.tfstate" 6 | encrypt = true 7 | } 8 | required_providers { 9 | aws = { 10 | source = "hashicorp/aws" 11 | version = "~> 4.67.0" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/pkg/utils/url.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "net/url" 4 | 5 | func IsHTTPSURL(href string) bool { 6 | u, err := url.ParseRequestURI(href) 7 | if err != nil { 8 | return false 9 | } 10 | return u.Scheme == "https" 11 | } 12 | 13 | func IsURL(str string) bool { 14 | _, err := url.ParseRequestURI(str) 15 | return err == nil 16 | } 17 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lgtm-generator-backend", 3 | "license": "MIT", 4 | "private": true, 5 | "engines": { 6 | "node": "18.x" 7 | }, 8 | "scripts": { 9 | "deploy": "serverless deploy --verbose", 10 | "remove": "serverless remove --verbose" 11 | }, 12 | "devDependencies": { 13 | "serverless": "3.32.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/components/utils/Field.tsx: -------------------------------------------------------------------------------- 1 | import { Box, BoxProps } from '@mui/material'; 2 | import React from 'react'; 3 | 4 | export type FieldProps = BoxProps; 5 | 6 | const Field: React.FC = React.memo(props => { 7 | return ; 8 | }); 9 | 10 | Field.displayName = 'Field'; 11 | 12 | export default Field; 13 | -------------------------------------------------------------------------------- /.github/workflows/release_prod.yml: -------------------------------------------------------------------------------- 1 | name: Release (prod) 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | uses: ./.github/workflows/_build.yml 11 | secrets: inherit 12 | 13 | release: 14 | needs: [build] 15 | uses: ./.github/workflows/_release.yml 16 | with: 17 | stage: prod 18 | secrets: inherit 19 | -------------------------------------------------------------------------------- /frontend/src/models/report.ts: -------------------------------------------------------------------------------- 1 | export type Report = { 2 | id: string; 3 | lgtmId: string; 4 | type: ReportType; 5 | text: string; 6 | createdAt: Date; 7 | }; 8 | 9 | export const ReportType = { 10 | illegal: 'illegal', 11 | inappropriate: 'inappropriate', 12 | other: 'other', 13 | } as const; 14 | 15 | export type ReportType = typeof ReportType[keyof typeof ReportType]; 16 | -------------------------------------------------------------------------------- /backend/pkg/entities/image.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import "strings" 4 | 5 | type Image struct { 6 | Title string `json:"title"` 7 | URL string `json:"url"` 8 | } 9 | 10 | type Images []*Image 11 | 12 | type ImagesSearchInput struct { 13 | Query string `form:"q"` 14 | } 15 | 16 | func (ipt *ImagesSearchInput) Valid() bool { 17 | if strings.TrimSpace(ipt.Query) == "" { 18 | return false 19 | } 20 | return true 21 | } 22 | -------------------------------------------------------------------------------- /terraform/app/modules/aws/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | app = "lgtm-generator" 3 | prefix = "${local.app}-${var.stage}" 4 | prefix_backend = "${local.app}-backend-${var.stage}" 5 | 6 | domain = "lgtmgen.org" 7 | 8 | sub_domain = var.stage == "prod" ? "" : "${var.stage}." 9 | api_domain = "${local.sub_domain}api.${local.domain}" 10 | images_domain = "${local.sub_domain}images.${local.domain}" 11 | } 12 | -------------------------------------------------------------------------------- /backend/pkg/utils/base64.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/base64" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func IsBase64(str string) bool { 10 | _, err := base64.StdEncoding.DecodeString(str) 11 | return err == nil 12 | } 13 | 14 | func Base64Decode(str string) ([]byte, error) { 15 | b, err := base64.StdEncoding.DecodeString(str) 16 | if err != nil { 17 | return nil, errors.WithStack(err) 18 | } 19 | return b, nil 20 | } 21 | -------------------------------------------------------------------------------- /terraform/app/tfsec.yml: -------------------------------------------------------------------------------- 1 | minimum_severity: HIGH 2 | 3 | exclude: 4 | - aws-s3-enable-versioning 5 | - aws-s3-enable-bucket-logging 6 | - aws-s3-encryption-customer-key 7 | - aws-cloudfront-enable-logging 8 | - aws-cloudfront-enable-waf 9 | 10 | # TODO: 設定してるのに警告される。 tfsec 側の不具合? 11 | # 一旦 exclude しておく。 12 | - aws-s3-no-public-buckets 13 | - aws-s3-ignore-public-acls 14 | - aws-s3-block-public-policy 15 | - aws-s3-block-public-acls 16 | -------------------------------------------------------------------------------- /backend/pkg/controllers/health_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | type HealthController struct { 8 | Renderer *Renderer 9 | } 10 | 11 | func NewHealthController() *HealthController { 12 | return &HealthController{ 13 | Renderer: NewRenderer(), 14 | } 15 | } 16 | 17 | func (ctrl *HealthController) Standard(ctx *gin.Context) { 18 | ctrl.Renderer.OK(ctx, map[string]string{"status": "ok"}) 19 | } 20 | -------------------------------------------------------------------------------- /terraform/app/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.2.9" 3 | 4 | backend "s3" { 5 | workspace_key_prefix = "workspaces" 6 | region = "us-east-1" 7 | bucket = "lgtm-generator-tfstates" 8 | key = "terraform.tfstate" 9 | encrypt = true 10 | } 11 | required_providers { 12 | aws = { 13 | source = "hashicorp/aws" 14 | version = "~> 4.67.0" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/release_dev.yml: -------------------------------------------------------------------------------- 1 | name: Release (dev) 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - main 7 | 8 | jobs: 9 | build: 10 | uses: ./.github/workflows/_build.yml 11 | secrets: inherit 12 | 13 | release: 14 | needs: [build] 15 | uses: ./.github/workflows/_release.yml 16 | with: 17 | stage: dev 18 | secrets: inherit 19 | 20 | e2e: 21 | needs: [release] 22 | uses: ./.github/workflows/_e2e.yml 23 | secrets: inherit 24 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Serverless directories 2 | .serverless 3 | 4 | # golang output binary directory 5 | /build/ 6 | 7 | # golang vendor (dependencies) directory 8 | vendor 9 | 10 | # Binaries for programs and plugins 11 | *.exe 12 | *.exe~ 13 | *.dll 14 | *.so 15 | *.dylib 16 | 17 | # Test binary, build with `go test -c` 18 | *.test 19 | 20 | # Output of the go coverage tool, specifically when used with LiteIDE 21 | cover.* 22 | 23 | /node_modules/ 24 | /.env 25 | /.dynamodb 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /terraform/app/modules/aws/ecr.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecr_lifecycle_policy" "app" { 2 | repository = "serverless-${local.prefix_backend}" 3 | policy = jsonencode({ 4 | rules = [ 5 | { 6 | rulePriority = 1, 7 | action = { type = "expire" } 8 | selection = { 9 | tagStatus = "untagged" 10 | countType = "sinceImagePushed" 11 | countUnit = "days" 12 | countNumber = 1 13 | } 14 | } 15 | ] 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /terraform/app/.tflint.hcl: -------------------------------------------------------------------------------- 1 | config { 2 | module = true 3 | } 4 | 5 | plugin "aws" { 6 | enabled = true 7 | version = "0.23.1" 8 | source = "github.com/terraform-linters/tflint-ruleset-aws" 9 | } 10 | 11 | rule "terraform_comment_syntax" { 12 | enabled = true 13 | } 14 | 15 | rule "terraform_deprecated_index" { 16 | enabled = true 17 | } 18 | 19 | rule "terraform_deprecated_interpolation" { 20 | enabled = true 21 | } 22 | 23 | rule "terraform_unused_declarations" { 24 | enabled = true 25 | } 26 | -------------------------------------------------------------------------------- /backend/pkg/infrastructures/dynamodb/dynamodb.go: -------------------------------------------------------------------------------- 1 | package dynamodb 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/aws/session" 8 | "github.com/guregu/dynamo" 9 | ) 10 | 11 | func New() *dynamo.DB { 12 | awscfg := &aws.Config{Region: aws.String("us-east-1")} 13 | if os.Getenv("STAGE") == "local" { 14 | awscfg.Endpoint = aws.String("http://localhost:8000") 15 | } 16 | sess := session.Must(session.NewSession(awscfg)) 17 | 18 | return dynamo.New(sess) 19 | } 20 | -------------------------------------------------------------------------------- /backend/pkg/controllers/errcode.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | type ErrCode string 4 | 5 | const ( 6 | ErrCodeInvalidJSON ErrCode = "INVALID_JSON" 7 | ErrCodeInvalidInput ErrCode = "INVALID_INPUT" 8 | ErrCodeInvalidQuery ErrCode = "INVALID_QUERY" 9 | ErrCodeUnsupportedImageFormat ErrCode = "UNSUPPORTED_IMAGE_FORMAT" 10 | 11 | ErrCodeForbidden ErrCode = "FORBIDDEN" 12 | ErrCodeNotFound ErrCode = "NOT_FOUND" 13 | 14 | ErrCodeInternalServerError ErrCode = "INTERNAL_SERVER_ERROR" 15 | ) 16 | -------------------------------------------------------------------------------- /terraform/app/modules/aws/apigateway.tf: -------------------------------------------------------------------------------- 1 | data "aws_api_gateway_rest_api" "api" { 2 | name = local.prefix_backend 3 | } 4 | 5 | resource "aws_api_gateway_domain_name" "api" { 6 | certificate_arn = aws_acm_certificate.api.arn 7 | domain_name = local.api_domain 8 | security_policy = "TLS_1_2" 9 | } 10 | 11 | resource "aws_api_gateway_base_path_mapping" "api" { 12 | api_id = data.aws_api_gateway_rest_api.api.id 13 | stage_name = var.stage 14 | domain_name = aws_api_gateway_domain_name.api.domain_name 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/recoil/atoms.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | import { DataStorage } from '@/lib/dataStorage'; 3 | import { Image } from '@/models/image'; 4 | import { Lgtm } from '@/models/lgtm'; 5 | 6 | export const lgtmsState = atom({ 7 | key: 'lgtmsState', 8 | default: [], 9 | }); 10 | 11 | export const imagesState = atom({ 12 | key: 'imagesState', 13 | default: [], 14 | }); 15 | 16 | export const favoriteIdsState = atom({ 17 | key: 'favoriteIdsState', 18 | default: DataStorage.getFavoriteIds(), 19 | }); 20 | -------------------------------------------------------------------------------- /frontend/src/hooks/i18n.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import React from 'react'; 3 | 4 | export const createTranslate = < 5 | T extends { [key: string]: { ja: React.ReactNode; en: React.ReactNode } }, 6 | >( 7 | translate: T, 8 | ) => { 9 | const useTranslate = () => { 10 | const { locale } = useRouter(); 11 | const t = React.useCallback( 12 | (key: keyof T) => { 13 | return translate[key][locale]; 14 | }, 15 | [locale], 16 | ); 17 | 18 | return { t }; 19 | }; 20 | return useTranslate; 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/src/components/utils/Form.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | 3 | export type FormProps = React.HTMLProps; 4 | 5 | const Form: React.FC = React.memo(props => { 6 | const { onSubmit, ...formProps } = props; 7 | 8 | const handleSubmit = useCallback( 9 | (e: React.FormEvent) => { 10 | e.preventDefault(); 11 | onSubmit?.(e); 12 | }, 13 | [onSubmit], 14 | ); 15 | 16 | return
; 17 | }); 18 | 19 | Form.displayName = 'Form'; 20 | 21 | export default Form; 22 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "LGTM", 3 | "name": "LGTM Generator", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "browser", 23 | "theme_color": "#1E90FF", 24 | "background_color": "#1E90FF" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/hooks/translateHooks.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { useMemo } from 'react'; 3 | import { en } from '@/locales/en'; 4 | import { ja } from '@/locales/ja'; 5 | import { Translate } from '@/locales/translate'; 6 | 7 | /** 8 | * @deprecated 9 | */ 10 | export const useTranslate = (): { t: Translate; locale: string } => { 11 | const { locale } = useRouter(); 12 | 13 | const t = useMemo(() => { 14 | switch (locale) { 15 | case 'en': 16 | return en; 17 | default: 18 | return ja; 19 | } 20 | }, [locale]); 21 | 22 | return { locale, t }; 23 | }; 24 | -------------------------------------------------------------------------------- /e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lgtm-generator-e2e", 3 | "license": "MIT", 4 | "private": true, 5 | "scripts": { 6 | "open:local": "cypress open --config-file cypress.local.config.ts", 7 | "open:dev": "cypress open --config-file cypress.dev.config.ts", 8 | "start:local": "cypress run --config-file cypress.local.config.ts", 9 | "start:dev": "cypress run --config-file cypress.dev.config.ts" 10 | }, 11 | "devDependencies": { 12 | "@types/node": "18.16.18", 13 | "cypress": "10.11.0", 14 | "cypress-file-upload": "5.0.8", 15 | "prettier": "2.8.8", 16 | "typescript": "4.9.5" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/lib/emotion.ts: -------------------------------------------------------------------------------- 1 | import createCache, { EmotionCache, Options } from '@emotion/cache'; 2 | 3 | /* 4 | * https://github.com/mui/material-ui/blob/master/examples/material-next-ts/src/createEmotionCache.ts 5 | */ 6 | 7 | const isBrowser = typeof document !== 'undefined'; 8 | 9 | export const createEmotionCache = (): EmotionCache => { 10 | const options: Options = { key: 'mui-style' }; 11 | if (isBrowser) { 12 | const emotionInsertionPoint = document.querySelector( 13 | 'meta[name="emotion-insertion-point"]', 14 | ); 15 | options.insertionPoint = emotionInsertionPoint ?? undefined; 16 | } 17 | 18 | return createCache(options); 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/src/components/model/lgtm/LgtmCardList.tsx: -------------------------------------------------------------------------------- 1 | import { Grid } from '@mui/material'; 2 | import React from 'react'; 3 | import LgtmCard from '@/components/model/lgtm/LgtmCard'; 4 | 5 | type LgtmCardListProps = { 6 | ids: string[]; 7 | }; 8 | 9 | const LgtmCardList: React.FC = React.memo(props => { 10 | const { ids } = props; 11 | 12 | return ( 13 | 14 | {ids.map(id => ( 15 | 16 | 17 | 18 | ))} 19 | 20 | ); 21 | }); 22 | 23 | LgtmCardList.displayName = 'LgtmCardList'; 24 | 25 | export default LgtmCardList; 26 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | }, 7 | "target": "es5", 8 | "lib": ["dom", "dom.iterable", "esnext"], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /backend/pkg/handlers/api/lambda/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-lambda-go/events" 7 | "github.com/aws/aws-lambda-go/lambda" 8 | ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin" 9 | "github.com/koki-develop/lgtm-generator/backend/pkg/infrastructures/router" 10 | ) 11 | 12 | var ginLambda *ginadapter.GinLambda 13 | 14 | func main() { 15 | lambda.Start(handler) 16 | } 17 | 18 | func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 19 | return ginLambda.ProxyWithContext(ctx, req) 20 | } 21 | 22 | func init() { 23 | r := router.New() 24 | 25 | ginLambda = ginadapter.New(r) 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/components/utils/LoadableButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps, CircularProgress } from '@mui/material'; 2 | import React from 'react'; 3 | 4 | export type LoadableButtonProps = ButtonProps & { 5 | loading: boolean; 6 | }; 7 | 8 | const LoadableButton: React.FC = React.memo(props => { 9 | const { loading, children, ...buttonProps } = props; 10 | 11 | return ( 12 | 16 | ); 17 | }); 18 | 19 | LoadableButton.displayName = 'LoadableButton'; 20 | 21 | export default LoadableButton; 22 | -------------------------------------------------------------------------------- /frontend/src/components/pages/NotFound/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from '@mui/material'; 2 | import React from 'react'; 3 | import { useTranslate } from '@/hooks/translateHooks'; 4 | import Layout from '@/components/Layout'; 5 | 6 | const NotFound: React.FC = React.memo(() => { 7 | const { t } = useTranslate(); 8 | 9 | return ( 10 | 11 | theme.typography.h5.fontSize, 14 | fontWeight: 'bold', 15 | textAlign: 'center', 16 | }} 17 | > 18 | {t.NOT_FOUND} 19 | 20 | 21 | ); 22 | }); 23 | 24 | NotFound.displayName = 'NotFound'; 25 | 26 | export default NotFound; 27 | -------------------------------------------------------------------------------- /backend/.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | tmp_dir = "tmp" 3 | 4 | [build] 5 | bin = "./build/api" 6 | cmd = "go build -o ./build/api ./pkg/handlers/api/local/" 7 | delay = 1000 8 | exclude_dir = ["assets", "tmp", "vendor", "node_modules"] 9 | exclude_file = [] 10 | exclude_regex = [] 11 | exclude_unchanged = false 12 | follow_symlink = false 13 | full_bin = "" 14 | include_dir = [] 15 | include_ext = ["go", "tpl", "tmpl", "html"] 16 | kill_delay = "0s" 17 | log = "build-errors.log" 18 | send_interrupt = false 19 | stop_on_error = true 20 | 21 | [color] 22 | app = "" 23 | build = "yellow" 24 | main = "magenta" 25 | runner = "green" 26 | watcher = "cyan" 27 | 28 | [log] 29 | time = false 30 | 31 | [misc] 32 | clean_on_exit = false 33 | -------------------------------------------------------------------------------- /.github/workflows/_e2e.yml: -------------------------------------------------------------------------------- 1 | name: E2E 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | CYPRESS_PROJECT_ID: 7 | required: true 8 | CYPRESS_RECORD_KEY: 9 | required: true 10 | 11 | jobs: 12 | e2e: 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | working-directory: e2e 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: cypress-io/github-action@v5.8.1 20 | with: 21 | config-file: cypress.dev.config.ts 22 | record: true 23 | working-directory: e2e 24 | env: 25 | CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }} 26 | CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /frontend/src/components/utils/Link.tsx: -------------------------------------------------------------------------------- 1 | import MuiLink, { LinkProps as MuiLinkProps } from '@mui/material/Link'; 2 | import NextLink from 'next/link'; 3 | import React from 'react'; 4 | 5 | type LinkProps = MuiLinkProps & { 6 | locale?: string; 7 | external?: boolean; 8 | }; 9 | 10 | const Link: React.FC = React.memo(props => { 11 | const { external, locale, ...linkProps } = props; 12 | 13 | if (external) { 14 | return ; 15 | } 16 | 17 | const { href, ...otherLinkProps } = linkProps; 18 | 19 | return ( 20 | 21 | 22 | 23 | ); 24 | }); 25 | 26 | Link.displayName = 'Link'; 27 | 28 | export default Link; 29 | -------------------------------------------------------------------------------- /frontend/src/components/model/image/ImageCardList.tsx: -------------------------------------------------------------------------------- 1 | import Grid from '@mui/material/Grid'; 2 | import React from 'react'; 3 | import { Image } from '@/models/image'; 4 | import ImageCard from './ImageCard'; 5 | 6 | type ImageCardListProps = { 7 | images: Image[]; 8 | onClickImage: (image: Image) => void; 9 | }; 10 | 11 | const ImageCardList: React.FC = React.memo(props => { 12 | const { images, onClickImage } = props; 13 | 14 | return ( 15 | 16 | {images.map(image => ( 17 | 18 | 19 | 20 | ))} 21 | 22 | ); 23 | }); 24 | 25 | ImageCardList.displayName = 'ImageCardList'; 26 | 27 | export default ImageCardList; 28 | -------------------------------------------------------------------------------- /backend/pkg/handlers/deletelgtm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/aws/aws-lambda-go/lambda" 8 | "github.com/koki-develop/lgtm-generator/backend/pkg/infrastructures/dynamodb" 9 | "github.com/koki-develop/lgtm-generator/backend/pkg/infrastructures/s3" 10 | "github.com/koki-develop/lgtm-generator/backend/pkg/repositories" 11 | ) 12 | 13 | type Event struct { 14 | LGTMID string `json:"lgtm_id"` 15 | } 16 | 17 | func handler(e Event) error { 18 | db := dynamodb.New() 19 | bucket := fmt.Sprintf("lgtm-generator-backend-%s-images", os.Getenv("STAGE")) 20 | s3client := s3.New(bucket) 21 | repo := repositories.NewLGTMsRepository(s3client, db) 22 | if err := repo.Delete(e.LGTMID); err != nil { 23 | return err 24 | } 25 | return nil 26 | } 27 | 28 | func main() { 29 | lambda.Start(handler) 30 | } 31 | -------------------------------------------------------------------------------- /backend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | bucket: 5 | image: minio/minio:latest 6 | ports: 7 | - 9000:9000 8 | - 9001:9001 9 | volumes: 10 | - bucket_data:/export 11 | environment: 12 | MINIO_ROOT_USER: DUMMY_AWS_ACCESS_KEY_ID 13 | MINIO_ROOT_PASSWORD: DUMMY_AWS_SECRET_ACCESS_KEY 14 | entrypoint: sh 15 | command: | 16 | -c " 17 | mkdir -p /export/lgtm-generator-backend-local-images 18 | /usr/bin/docker-entrypoint.sh server /export --console-address ':9001' 19 | " 20 | 21 | dynamodb: 22 | image: amazon/dynamodb-local:1.22.0 23 | user: root 24 | command: -jar DynamoDBLocal.jar -sharedDb -dbPath /data 25 | ports: 26 | - 8000:8000 27 | volumes: 28 | - dynamodb_data:/data 29 | 30 | volumes: 31 | bucket_data: 32 | dynamodb_data: 33 | -------------------------------------------------------------------------------- /backend/containers/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 as build 2 | WORKDIR /var/task 3 | 4 | RUN apt update \ 5 | && apt install -y \ 6 | imagemagick \ 7 | libmagickwand-dev 8 | 9 | COPY go.mod go.sum ./ 10 | RUN go mod download -x 11 | COPY . . 12 | RUN GO111MODULE=on GOOS=linux go build -ldflags="-s -w" -o build/api pkg/handlers/api/lambda/main.go \ 13 | && GO111MODULE=on GOOS=linux go build -ldflags="-s -w" -o build/deletelgtm pkg/handlers/deletelgtm/main.go 14 | 15 | ENTRYPOINT [] 16 | 17 | # --------------------------- 18 | 19 | FROM golang:1.19 20 | WORKDIR /var/task 21 | 22 | RUN apt update \ 23 | && apt install -y \ 24 | imagemagick \ 25 | libmagickwand-dev 26 | COPY --from=build /var/task/pkg/static/ /var/task/pkg/static/ 27 | COPY --from=build /var/task/build/api /var/task/build/api 28 | COPY --from=build /var/task/build/deletelgtm /var/task/build/deletelgtm 29 | 30 | ENTRYPOINT [] 31 | -------------------------------------------------------------------------------- /frontend/src/components/Layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box'; 2 | import Container from '@mui/material/Container'; 3 | import React from 'react'; 4 | import Meta from '@/components/utils/Meta'; 5 | import Footer from './Footer'; 6 | import Header from './Header'; 7 | 8 | type LayoutProps = { 9 | children: React.ReactNode; 10 | title?: string; 11 | }; 12 | 13 | const Layout: React.FC = React.memo(props => { 14 | const { children, title } = props; 15 | 16 | return ( 17 | theme.palette.primary.light, 20 | minHeight: '100vh', 21 | }} 22 | > 23 | 24 |
25 | 26 | {children} 27 | 28 |