wip: add api auth and /resume endpoint
This commit is contained in:
parent
22e2f6005f
commit
aaec8ec08d
23 changed files with 1244 additions and 271 deletions
287
.gitignore
vendored
287
.gitignore
vendored
|
|
@ -1,277 +1,22 @@
|
|||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
# Ignore everything
|
||||
*
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
# But don't ignore these files...
|
||||
!/.gitignore
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
!*.go
|
||||
!go.sum
|
||||
!go.mod
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
!README.md
|
||||
!LICENSE
|
||||
!apischema/openapi.yaml
|
||||
!apischema/resumeschema.json
|
||||
!configs/config.yml
|
||||
!configs/example.db.json
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
# !Makefile
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# ---> Linux
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
# ---> JetBrains
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
# ---> Vim
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
!*.svg # comment out if you don't need vector files
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
*~
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
# ...even if they are in subdirectories
|
||||
!*/
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
# cvvvv
|
||||
|
||||
WIP
|
||||
260
apischema/openapi.yaml
Normal file
260
apischema/openapi.yaml
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
openapi: 3.1.0
|
||||
x-stoplight:
|
||||
id: 8awcrxb6k3l5z
|
||||
info:
|
||||
contact:
|
||||
email: catalin@roboces.dev
|
||||
name: cătălin
|
||||
license:
|
||||
name: GNU General Public License v3.0 or later
|
||||
identifier: GPL-3.0-or-later
|
||||
title: CVVV
|
||||
description: JSON Resume Schema Explorer
|
||||
version: 0.1.0
|
||||
servers:
|
||||
- url: 'https://cvvvvv.roboces.dev/api/v1'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
paths:
|
||||
/ping:
|
||||
get:
|
||||
summary: Healthcheck
|
||||
description: Check if the server is alive
|
||||
operationId: getPing
|
||||
responses:
|
||||
'200':
|
||||
description: Ping
|
||||
content:
|
||||
schema:
|
||||
type: string
|
||||
example: pong
|
||||
x-stoplight:
|
||||
id: qrrj5n6zobz3p
|
||||
/usage:
|
||||
get:
|
||||
summary: Get usage
|
||||
description: Show usage instructions
|
||||
operationId: getUsage
|
||||
responses:
|
||||
'200':
|
||||
description: Usage
|
||||
content:
|
||||
schema:
|
||||
type: string
|
||||
example: pong
|
||||
x-stoplight:
|
||||
id: xt6go87v6s1rz
|
||||
/resume:
|
||||
get:
|
||||
summary: Get resume
|
||||
description: Retrieve the full JSON resume
|
||||
operationId: getResume
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Resume
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./resumeschema.json
|
||||
x-stoplight:
|
||||
id: w9tzzy49yf2sw
|
||||
/resume/basics:
|
||||
get:
|
||||
summary: Get basics
|
||||
description: Retrieve the basics section from the JSON resume
|
||||
operationId: getResumeBasics
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Resume basics
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./resumeschema.json#/properties/basics
|
||||
x-stoplight:
|
||||
id: 40upzt6ummt0i
|
||||
/resume/work:
|
||||
get:
|
||||
summary: Get work
|
||||
description: Retrieve the work section from the JSON resume
|
||||
operationId: getResumeWork
|
||||
security:
|
||||
- ApiKeyAuth
|
||||
responses:
|
||||
'200':
|
||||
description: Resume work
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./resumeschema.json#/properties/work
|
||||
x-stoplight:
|
||||
id: 310a662ft52e4
|
||||
/resume/volunteer:
|
||||
get:
|
||||
summary: Get volunteer
|
||||
description: Retrieve the volunteer section from the JSON resume
|
||||
operationId: getResumeVolunteer
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Resume volunteer
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./resumeschema.json#/properties/volunteer
|
||||
x-stoplight:
|
||||
id: 5hqoylsv7zfu3
|
||||
/resume/education:
|
||||
get:
|
||||
summary: Get education
|
||||
description: Retrieve the education section from the JSON resume
|
||||
operationId: getResumeEducation
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Resume education
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./resumeschema.json#/properties/education
|
||||
x-stoplight:
|
||||
id: u4jgzg5nksvps
|
||||
/resume/awards:
|
||||
get:
|
||||
summary: Get awards
|
||||
description: Retrieve the awards section from the JSON resume
|
||||
operationId: getResumeAwards
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Resume awards
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./resumeschema.json#/properties/awards
|
||||
x-stoplight:
|
||||
id: 0zjhsoqee1cvj
|
||||
/resume/certificates:
|
||||
get:
|
||||
summary: Get certificates
|
||||
description: Retrieve the certificates section from the JSON resume
|
||||
operationId: getResumeCertificates
|
||||
security:
|
||||
- ApiKeyAuth
|
||||
responses:
|
||||
'200':
|
||||
description: Resume certificates
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./resumeschema.json#/properties/certificates
|
||||
x-stoplight:
|
||||
id: wy17alifr6isl
|
||||
/resume/publications:
|
||||
get:
|
||||
summary: Get publications
|
||||
description: Retrieve the publications section from the JSON resume
|
||||
operationId: getResumePublications
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Resume publications
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./resumeschema.json#/properties/publications
|
||||
x-stoplight:
|
||||
id: tr5v58qwzhjah
|
||||
/resume/skills:
|
||||
get:
|
||||
summary: Get skills
|
||||
description: Retrieve the skills section from the JSON resume
|
||||
operationId: getResumeSkills
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Resume skills
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./resumeschema.json#/properties/skills
|
||||
x-stoplight:
|
||||
id: 2of3u7x07mh02
|
||||
/resume/languages:
|
||||
get:
|
||||
summary: Get languages
|
||||
description: Retrieve the languages section from the JSON resume
|
||||
operationId: getResumeLanguages
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Resume languages
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./resumeschema.json#/properties/languages
|
||||
x-stoplight:
|
||||
id: 22ndcxgbjnwhh
|
||||
/resume/interests:
|
||||
get:
|
||||
summary: Get interests
|
||||
description: Retrieve the interests section from the JSON resume
|
||||
operationId: getResumeInterests
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Resume interests
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./resumeschema.json#/properties/interests
|
||||
x-stoplight:
|
||||
id: 7dv8xl1apkm2y
|
||||
/resume/references:
|
||||
get:
|
||||
summary: Get references
|
||||
description: Retrieve the references section from the JSON resume
|
||||
operationId: getResumeReferences
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Resume references
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./resumeschema.json#/properties/references
|
||||
x-stoplight:
|
||||
id: 2wl5vh02k4jpo
|
||||
/resume/projects:
|
||||
get:
|
||||
summary: Get projects
|
||||
description: Retrieve the projects section from the JSON resume
|
||||
operationId: getResumeProjects
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Resume projects
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ./resumeschema.json#/properties/projects
|
||||
x-stoplight:
|
||||
id: 0ck9me9tw4m3e
|
||||
components:
|
||||
securitySchemes:
|
||||
ApiKeyAuth:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-API-KEY
|
||||
9
cmd/app/main.go
Normal file
9
cmd/app/main.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
import "git.roboces.dev/catalin/cvvvvv/internal/app"
|
||||
|
||||
const configsDir = "configs"
|
||||
|
||||
func main() {
|
||||
app.Run(configsDir)
|
||||
}
|
||||
110
configs/example.db.json
Normal file
110
configs/example.db.json
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"basics": {
|
||||
"name": "John Doe",
|
||||
"label": "Programmer",
|
||||
"image": "",
|
||||
"email": "john@gmail.com",
|
||||
"phone": "(912) 555-4321",
|
||||
"url": "https://johndoe.com",
|
||||
"summary": "A summary of John Doe…",
|
||||
"location": {
|
||||
"address": "2712 Broadway St",
|
||||
"postalCode": "CA 94115",
|
||||
"city": "San Francisco",
|
||||
"countryCode": "US",
|
||||
"region": "California"
|
||||
},
|
||||
"profiles": [{
|
||||
"network": "Twitter",
|
||||
"username": "john",
|
||||
"url": "https://twitter.com/john"
|
||||
}]
|
||||
},
|
||||
"work": [{
|
||||
"name": "Company",
|
||||
"position": "President",
|
||||
"url": "https://company.com",
|
||||
"startDate": "2013-01-01",
|
||||
"endDate": "2014-01-01",
|
||||
"summary": "Description…",
|
||||
"highlights": [
|
||||
"Started the company"
|
||||
]
|
||||
}],
|
||||
"volunteer": [{
|
||||
"organization": "Organization",
|
||||
"position": "Volunteer",
|
||||
"url": "https://organization.com/",
|
||||
"startDate": "2012-01-01",
|
||||
"endDate": "2013-01-01",
|
||||
"summary": "Description…",
|
||||
"highlights": [
|
||||
"Awarded 'Volunteer of the Month'"
|
||||
]
|
||||
}],
|
||||
"education": [{
|
||||
"institution": "University",
|
||||
"url": "https://institution.com/",
|
||||
"area": "Software Development",
|
||||
"studyType": "Bachelor",
|
||||
"startDate": "2011-01-01",
|
||||
"endDate": "2013-01-01",
|
||||
"score": "4.0",
|
||||
"courses": [
|
||||
"DB1101 - Basic SQL"
|
||||
]
|
||||
}],
|
||||
"awards": [{
|
||||
"title": "Award",
|
||||
"date": "2014-11-01",
|
||||
"awarder": "Company",
|
||||
"summary": "There is no spoon."
|
||||
}],
|
||||
"certificates": [{
|
||||
"name": "Certificate",
|
||||
"date": "2021-11-07",
|
||||
"issuer": "Company",
|
||||
"url": "https://certificate.com"
|
||||
}],
|
||||
"publications": [{
|
||||
"name": "Publication",
|
||||
"publisher": "Company",
|
||||
"releaseDate": "2014-10-01",
|
||||
"url": "https://publication.com",
|
||||
"summary": "Description…"
|
||||
}],
|
||||
"skills": [{
|
||||
"name": "Web Development",
|
||||
"level": "Master",
|
||||
"keywords": [
|
||||
"HTML",
|
||||
"CSS",
|
||||
"JavaScript"
|
||||
]
|
||||
}],
|
||||
"languages": [{
|
||||
"language": "English",
|
||||
"fluency": "Native speaker"
|
||||
}],
|
||||
"interests": [{
|
||||
"name": "Wildlife",
|
||||
"keywords": [
|
||||
"Ferrets",
|
||||
"Unicorns"
|
||||
]
|
||||
}],
|
||||
"references": [{
|
||||
"name": "Jane Doe",
|
||||
"reference": "Reference…"
|
||||
}],
|
||||
"projects": [{
|
||||
"name": "Project",
|
||||
"startDate": "2019-01-01",
|
||||
"endDate": "2021-01-01",
|
||||
"description": "Description...",
|
||||
"highlights": [
|
||||
"Won award at AIHacks 2016"
|
||||
],
|
||||
"url": "https://project.com/"
|
||||
}]
|
||||
}
|
||||
74
internal/app/app.go
Normal file
74
internal/app/app.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/config"
|
||||
delivery "git.roboces.dev/catalin/cvvvvv/internal/delivery/http"
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/domain"
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/repo"
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/server"
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/service"
|
||||
"git.roboces.dev/catalin/cvvvvv/pkg/logger"
|
||||
)
|
||||
|
||||
func loadResume(path string, resume *domain.Resume) error {
|
||||
datafile, err := os.Open(path)
|
||||
if err != nil {
|
||||
return errors.New("could not open database file")
|
||||
}
|
||||
decoder := json.NewDecoder(datafile)
|
||||
if decoder.Decode(&resume) != nil {
|
||||
return errors.New("could not decode database")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Run(configPath string) {
|
||||
cfg, err := config.Init(configPath)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
resume := domain.Resume{}
|
||||
if loadResume(cfg.Database, &resume) != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
repos := repo.NewRepos(resume)
|
||||
services := service.NewServices(service.Deps{
|
||||
Repos: repos,
|
||||
Config: cfg,
|
||||
})
|
||||
handlers := delivery.NewHandler(services)
|
||||
srv := server.NewServer(cfg, handlers.Init(cfg))
|
||||
|
||||
go func() {
|
||||
if err := srv.Run(); !errors.Is(err, http.ErrServerClosed) {
|
||||
logger.Errorf("error occurred while running http server: %s\n", err.Error())
|
||||
}
|
||||
}()
|
||||
logger.Info("Server started")
|
||||
|
||||
// Graceful Shutdown
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
|
||||
|
||||
<-quit
|
||||
|
||||
const timeout = 5 * time.Second
|
||||
|
||||
ctx, shutdown := context.WithTimeout(context.Background(), timeout)
|
||||
defer shutdown()
|
||||
|
||||
if err := srv.Stop(ctx); err != nil {
|
||||
logger.Errorf("failed to stop server: %v", err)
|
||||
}
|
||||
}
|
||||
43
internal/delivery/http/handler.go
Normal file
43
internal/delivery/http/handler.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/config"
|
||||
v1 "git.roboces.dev/catalin/cvvvvv/internal/delivery/http/v1"
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/service"
|
||||
"git.roboces.dev/catalin/cvvvvv/pkg/limiter"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
services *service.Services
|
||||
}
|
||||
|
||||
func NewHandler(services *service.Services) *Handler {
|
||||
return &Handler{
|
||||
services: services,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Init(cfg *config.Config) *gin.Engine {
|
||||
// Init gin handler
|
||||
router := gin.Default()
|
||||
|
||||
router.Use(
|
||||
gin.Recovery(),
|
||||
gin.Logger(),
|
||||
limiter.Limit(cfg.Limiter.RPS, cfg.Limiter.Burst, cfg.Limiter.TTL),
|
||||
corsMiddleware,
|
||||
)
|
||||
|
||||
h.initAPI(router)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func (h *Handler) initAPI(router *gin.Engine) {
|
||||
handlerV1 := v1.NewHandler(h.services)
|
||||
api := router.Group("/api")
|
||||
{
|
||||
handlerV1.Init(api)
|
||||
}
|
||||
}
|
||||
20
internal/delivery/http/middleware.go
Normal file
20
internal/delivery/http/middleware.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func corsMiddleware(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Methods", "*")
|
||||
c.Header("Access-Control-Allow-Headers", "*")
|
||||
c.Header("Content-Type", "application/json")
|
||||
|
||||
if c.Request.Method != "OPTIONS" {
|
||||
c.Next()
|
||||
} else {
|
||||
c.AbortWithStatus(http.StatusOK)
|
||||
}
|
||||
}
|
||||
25
internal/delivery/http/v1/handler.go
Normal file
25
internal/delivery/http/v1/handler.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
services *service.Services
|
||||
}
|
||||
|
||||
func NewHandler(services *service.Services) *Handler {
|
||||
return &Handler{
|
||||
services: services,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Init(api *gin.RouterGroup) {
|
||||
v1 := api.Group("/v1")
|
||||
{
|
||||
v1.GET("/usage", h.getUsage)
|
||||
v1.GET("/ping", h.getPing)
|
||||
v1.GET("/resume", h.getResume)
|
||||
}
|
||||
}
|
||||
30
internal/delivery/http/v1/middleware.go
Normal file
30
internal/delivery/http/v1/middleware.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func containsValue(m map[string]string, value string) (string, error) {
|
||||
for _, v := range m {
|
||||
if v == value {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
return value, errors.New("value is not in the provided map")
|
||||
}
|
||||
|
||||
func (h *Handler) VerifyAuthentication(ctx *gin.Context) (string, error) {
|
||||
header := ctx.GetHeader("X-API-KEY")
|
||||
if header != "" {
|
||||
return containsValue(h.services.Config.Auth.ApiKeys, header)
|
||||
}
|
||||
|
||||
key := ctx.Query("api_key")
|
||||
if key != "" {
|
||||
return containsValue(h.services.Config.Auth.ApiKeys, key)
|
||||
}
|
||||
|
||||
return "", errors.New("Missing API key")
|
||||
}
|
||||
12
internal/delivery/http/v1/ping.go
Normal file
12
internal/delivery/http/v1/ping.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *Handler) getPing(c *gin.Context) {
|
||||
c.Writer.Header().Set("Content-Type", "text/plain")
|
||||
c.String(http.StatusOK, "pong")
|
||||
}
|
||||
19
internal/delivery/http/v1/response.go
Normal file
19
internal/delivery/http/v1/response.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"git.roboces.dev/catalin/cvvvvv/pkg/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type dataResponse struct {
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func newResponse(c *gin.Context, statusCode int, message string) {
|
||||
logger.Error(message)
|
||||
c.AbortWithStatusJSON(statusCode, response{message})
|
||||
}
|
||||
24
internal/delivery/http/v1/resume.go
Normal file
24
internal/delivery/http/v1/resume.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/domain"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ResumeResponse struct {
|
||||
Resume domain.Resume `json:"resume"`
|
||||
}
|
||||
|
||||
func (h *Handler) getResume(ctx *gin.Context) {
|
||||
|
||||
_, err := h.VerifyAuthentication(ctx)
|
||||
if err != nil {
|
||||
newResponse(ctx, http.StatusUnauthorized, "Api key not provided or invalid")
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, ResumeResponse{
|
||||
Resume: h.services.Resumes.Get(),
|
||||
})
|
||||
}
|
||||
25
internal/delivery/http/v1/usage.go
Normal file
25
internal/delivery/http/v1/usage.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *Handler) getUsage(c *gin.Context) {
|
||||
c.Writer.Header().Set("Content-Type", "text/plain")
|
||||
message := fmt.Sprintf(`
|
||||
Hello! If you're seeing this message it means you are authorized to view my CV.
|
||||
You should have been provided an api key in order to query the different endpoints,
|
||||
which are not public. You can put the key into a X-API-KEY header or use it
|
||||
as a query param. Examples:
|
||||
|
||||
curl %s/api/v1/usage -H "X-API-KEY: verysecret"
|
||||
|
||||
curl %s/api/v1/usage?api_key=verysecret
|
||||
|
||||
`, c.Request.Host, c.Request.Host)
|
||||
|
||||
c.String(http.StatusOK, message)
|
||||
}
|
||||
316
internal/domain/resume.go
Normal file
316
internal/domain/resume.go
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
package domain
|
||||
|
||||
import "github.com/atombender/go-jsonschema/pkg/types"
|
||||
|
||||
// e.g. 2014-06-29
|
||||
type Iso8601 string
|
||||
|
||||
type Resume struct {
|
||||
// link to the version of the schema that can validate the resume
|
||||
Schema *string `json:"$schema,omitempty" yaml:"$schema,omitempty" mapstructure:"$schema,omitempty"`
|
||||
|
||||
// Specify any awards you have received throughout your professional career
|
||||
Awards []ResumeAwardsElem `json:"awards,omitempty" yaml:"awards,omitempty" mapstructure:"awards,omitempty"`
|
||||
|
||||
// Basics corresponds to the JSON schema field "basics".
|
||||
Basics *ResumeBasics `json:"basics,omitempty" yaml:"basics,omitempty" mapstructure:"basics,omitempty"`
|
||||
|
||||
// Specify any certificates you have received throughout your professional career
|
||||
Certificates []ResumeCertificatesElem `json:"certificates,omitempty" yaml:"certificates,omitempty" mapstructure:"certificates,omitempty"`
|
||||
|
||||
// Education corresponds to the JSON schema field "education".
|
||||
Education []ResumeEducationElem `json:"education,omitempty" yaml:"education,omitempty" mapstructure:"education,omitempty"`
|
||||
|
||||
// Interests corresponds to the JSON schema field "interests".
|
||||
Interests []ResumeInterestsElem `json:"interests,omitempty" yaml:"interests,omitempty" mapstructure:"interests,omitempty"`
|
||||
|
||||
// List any other languages you speak
|
||||
Languages []ResumeLanguagesElem `json:"languages,omitempty" yaml:"languages,omitempty" mapstructure:"languages,omitempty"`
|
||||
|
||||
// The schema version and any other tooling configuration lives here
|
||||
Meta *ResumeMeta `json:"meta,omitempty" yaml:"meta,omitempty" mapstructure:"meta,omitempty"`
|
||||
|
||||
// Specify career projects
|
||||
Projects []ResumeProjectsElem `json:"projects,omitempty" yaml:"projects,omitempty" mapstructure:"projects,omitempty"`
|
||||
|
||||
// Specify your publications through your career
|
||||
Publications []ResumePublicationsElem `json:"publications,omitempty" yaml:"publications,omitempty" mapstructure:"publications,omitempty"`
|
||||
|
||||
// List references you have received
|
||||
References []ResumeReferencesElem `json:"references,omitempty" yaml:"references,omitempty" mapstructure:"references,omitempty"`
|
||||
|
||||
// List out your professional skill-set
|
||||
Skills []ResumeSkillsElem `json:"skills,omitempty" yaml:"skills,omitempty" mapstructure:"skills,omitempty"`
|
||||
|
||||
// Volunteer corresponds to the JSON schema field "volunteer".
|
||||
Volunteer []ResumeVolunteerElem `json:"volunteer,omitempty" yaml:"volunteer,omitempty" mapstructure:"volunteer,omitempty"`
|
||||
|
||||
// Work corresponds to the JSON schema field "work".
|
||||
Work []ResumeWorkElem `json:"work,omitempty" yaml:"work,omitempty" mapstructure:"work,omitempty"`
|
||||
}
|
||||
|
||||
type ResumeAwardsElem struct {
|
||||
// e.g. Time Magazine
|
||||
Awarder *string `json:"awarder,omitempty" yaml:"awarder,omitempty" mapstructure:"awarder,omitempty"`
|
||||
|
||||
// Date corresponds to the JSON schema field "date".
|
||||
Date *Iso8601 `json:"date,omitempty" yaml:"date,omitempty" mapstructure:"date,omitempty"`
|
||||
|
||||
// e.g. Received for my work with Quantum Physics
|
||||
Summary *string `json:"summary,omitempty" yaml:"summary,omitempty" mapstructure:"summary,omitempty"`
|
||||
|
||||
// e.g. One of the 100 greatest minds of the century
|
||||
Title *string `json:"title,omitempty" yaml:"title,omitempty" mapstructure:"title,omitempty"`
|
||||
}
|
||||
|
||||
type ResumeBasics struct {
|
||||
// e.g. thomas@gmail.com
|
||||
Email *string `json:"email,omitempty" yaml:"email,omitempty" mapstructure:"email,omitempty"`
|
||||
|
||||
// URL (as per RFC 3986) to a image in JPEG or PNG format
|
||||
Image *string `json:"image,omitempty" yaml:"image,omitempty" mapstructure:"image,omitempty"`
|
||||
|
||||
// e.g. Web Developer
|
||||
Label *string `json:"label,omitempty" yaml:"label,omitempty" mapstructure:"label,omitempty"`
|
||||
|
||||
// Location corresponds to the JSON schema field "location".
|
||||
Location *ResumeBasicsLocation `json:"location,omitempty" yaml:"location,omitempty" mapstructure:"location,omitempty"`
|
||||
|
||||
// Name corresponds to the JSON schema field "name".
|
||||
Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"`
|
||||
|
||||
// Phone numbers are stored as strings so use any format you like, e.g.
|
||||
// 712-117-2923
|
||||
Phone *string `json:"phone,omitempty" yaml:"phone,omitempty" mapstructure:"phone,omitempty"`
|
||||
|
||||
// Specify any number of social networks that you participate in
|
||||
Profiles []ResumeBasicsProfilesElem `json:"profiles,omitempty" yaml:"profiles,omitempty" mapstructure:"profiles,omitempty"`
|
||||
|
||||
// Write a short 2-3 sentence biography about yourself
|
||||
Summary *string `json:"summary,omitempty" yaml:"summary,omitempty" mapstructure:"summary,omitempty"`
|
||||
|
||||
// URL (as per RFC 3986) to your website, e.g. personal homepage
|
||||
Url *string `json:"url,omitempty" yaml:"url,omitempty" mapstructure:"url,omitempty"`
|
||||
}
|
||||
|
||||
type ResumeBasicsLocation struct {
|
||||
// To add multiple address lines, use
|
||||
// . For example, 1234 Glücklichkeit Straße
|
||||
// Hinterhaus 5. Etage li.
|
||||
Address *string `json:"address,omitempty" yaml:"address,omitempty" mapstructure:"address,omitempty"`
|
||||
|
||||
// City corresponds to the JSON schema field "city".
|
||||
City *string `json:"city,omitempty" yaml:"city,omitempty" mapstructure:"city,omitempty"`
|
||||
|
||||
// code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN
|
||||
CountryCode *string `json:"countryCode,omitempty" yaml:"countryCode,omitempty" mapstructure:"countryCode,omitempty"`
|
||||
|
||||
// PostalCode corresponds to the JSON schema field "postalCode".
|
||||
PostalCode *string `json:"postalCode,omitempty" yaml:"postalCode,omitempty" mapstructure:"postalCode,omitempty"`
|
||||
|
||||
// The general region where you live. Can be a US state, or a province, for
|
||||
// instance.
|
||||
Region *string `json:"region,omitempty" yaml:"region,omitempty" mapstructure:"region,omitempty"`
|
||||
}
|
||||
|
||||
type ResumeBasicsProfilesElem struct {
|
||||
// e.g. Facebook or Twitter
|
||||
Network *string `json:"network,omitempty" yaml:"network,omitempty" mapstructure:"network,omitempty"`
|
||||
|
||||
// e.g. http://twitter.example.com/neutralthoughts
|
||||
Url *string `json:"url,omitempty" yaml:"url,omitempty" mapstructure:"url,omitempty"`
|
||||
|
||||
// e.g. neutralthoughts
|
||||
Username *string `json:"username,omitempty" yaml:"username,omitempty" mapstructure:"username,omitempty"`
|
||||
}
|
||||
|
||||
type ResumeCertificatesElem struct {
|
||||
// e.g. 1989-06-12
|
||||
Date *types.SerializableDate `json:"date,omitempty" yaml:"date,omitempty" mapstructure:"date,omitempty"`
|
||||
|
||||
// e.g. CNCF
|
||||
Issuer *string `json:"issuer,omitempty" yaml:"issuer,omitempty" mapstructure:"issuer,omitempty"`
|
||||
|
||||
// e.g. Certified Kubernetes Administrator
|
||||
Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"`
|
||||
|
||||
// e.g. http://example.com
|
||||
Url *string `json:"url,omitempty" yaml:"url,omitempty" mapstructure:"url,omitempty"`
|
||||
}
|
||||
|
||||
type ResumeEducationElem struct {
|
||||
// e.g. Arts
|
||||
Area *string `json:"area,omitempty" yaml:"area,omitempty" mapstructure:"area,omitempty"`
|
||||
|
||||
// List notable courses/subjects
|
||||
Courses []string `json:"courses,omitempty" yaml:"courses,omitempty" mapstructure:"courses,omitempty"`
|
||||
|
||||
// EndDate corresponds to the JSON schema field "endDate".
|
||||
EndDate *Iso8601 `json:"endDate,omitempty" yaml:"endDate,omitempty" mapstructure:"endDate,omitempty"`
|
||||
|
||||
// e.g. Massachusetts Institute of Technology
|
||||
Institution *string `json:"institution,omitempty" yaml:"institution,omitempty" mapstructure:"institution,omitempty"`
|
||||
|
||||
// grade point average, e.g. 3.67/4.0
|
||||
Score *string `json:"score,omitempty" yaml:"score,omitempty" mapstructure:"score,omitempty"`
|
||||
|
||||
// StartDate corresponds to the JSON schema field "startDate".
|
||||
StartDate *Iso8601 `json:"startDate,omitempty" yaml:"startDate,omitempty" mapstructure:"startDate,omitempty"`
|
||||
|
||||
// e.g. Bachelor
|
||||
StudyType *string `json:"studyType,omitempty" yaml:"studyType,omitempty" mapstructure:"studyType,omitempty"`
|
||||
|
||||
// e.g. http://facebook.example.com
|
||||
Url *string `json:"url,omitempty" yaml:"url,omitempty" mapstructure:"url,omitempty"`
|
||||
}
|
||||
|
||||
type ResumeInterestsElem struct {
|
||||
// Keywords corresponds to the JSON schema field "keywords".
|
||||
Keywords []string `json:"keywords,omitempty" yaml:"keywords,omitempty" mapstructure:"keywords,omitempty"`
|
||||
|
||||
// e.g. Philosophy
|
||||
Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"`
|
||||
}
|
||||
|
||||
type ResumeLanguagesElem struct {
|
||||
// e.g. Fluent, Beginner
|
||||
Fluency *string `json:"fluency,omitempty" yaml:"fluency,omitempty" mapstructure:"fluency,omitempty"`
|
||||
|
||||
// e.g. English, Spanish
|
||||
Language *string `json:"language,omitempty" yaml:"language,omitempty" mapstructure:"language,omitempty"`
|
||||
}
|
||||
|
||||
// The schema version and any other tooling configuration lives here
|
||||
type ResumeMeta struct {
|
||||
// URL (as per RFC 3986) to latest version of this document
|
||||
Canonical *string `json:"canonical,omitempty" yaml:"canonical,omitempty" mapstructure:"canonical,omitempty"`
|
||||
|
||||
// Using ISO 8601 with YYYY-MM-DDThh:mm:ss
|
||||
LastModified *string `json:"lastModified,omitempty" yaml:"lastModified,omitempty" mapstructure:"lastModified,omitempty"`
|
||||
|
||||
// A version field which follows semver - e.g. v1.0.0
|
||||
Version *string `json:"version,omitempty" yaml:"version,omitempty" mapstructure:"version,omitempty"`
|
||||
}
|
||||
|
||||
type ResumeProjectsElem struct {
|
||||
// Short summary of project. e.g. Collated works of 2017.
|
||||
Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"`
|
||||
|
||||
// EndDate corresponds to the JSON schema field "endDate".
|
||||
EndDate *Iso8601 `json:"endDate,omitempty" yaml:"endDate,omitempty" mapstructure:"endDate,omitempty"`
|
||||
|
||||
// Specify the relevant company/domain affiliations e.g. 'greenpeace',
|
||||
// 'corporationXYZ'
|
||||
Entity *string `json:"domain,omitempty" yaml:"domain,omitempty" mapstructure:"domain,omitempty"`
|
||||
|
||||
// Specify multiple features
|
||||
Highlights []string `json:"highlights,omitempty" yaml:"highlights,omitempty" mapstructure:"highlights,omitempty"`
|
||||
|
||||
// Specify special elements involved
|
||||
Keywords []string `json:"keywords,omitempty" yaml:"keywords,omitempty" mapstructure:"keywords,omitempty"`
|
||||
|
||||
// e.g. The World Wide Web
|
||||
Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"`
|
||||
|
||||
// Specify your role on this project or in company
|
||||
Roles []string `json:"roles,omitempty" yaml:"roles,omitempty" mapstructure:"roles,omitempty"`
|
||||
|
||||
// StartDate corresponds to the JSON schema field "startDate".
|
||||
StartDate *Iso8601 `json:"startDate,omitempty" yaml:"startDate,omitempty" mapstructure:"startDate,omitempty"`
|
||||
|
||||
// e.g. 'volunteering', 'presentation', 'talk', 'application', 'conference'
|
||||
Type *string `json:"type,omitempty" yaml:"type,omitempty" mapstructure:"type,omitempty"`
|
||||
|
||||
// e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html
|
||||
Url *string `json:"url,omitempty" yaml:"url,omitempty" mapstructure:"url,omitempty"`
|
||||
}
|
||||
|
||||
type ResumePublicationsElem struct {
|
||||
// e.g. The World Wide Web
|
||||
Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"`
|
||||
|
||||
// e.g. IEEE, Computer Magazine
|
||||
Publisher *string `json:"publisher,omitempty" yaml:"publisher,omitempty" mapstructure:"publisher,omitempty"`
|
||||
|
||||
// ReleaseDate corresponds to the JSON schema field "releaseDate".
|
||||
ReleaseDate *Iso8601 `json:"releaseDate,omitempty" yaml:"releaseDate,omitempty" mapstructure:"releaseDate,omitempty"`
|
||||
|
||||
// Short summary of publication. e.g. Discussion of the World Wide Web, HTTP,
|
||||
// HTML.
|
||||
Summary *string `json:"summary,omitempty" yaml:"summary,omitempty" mapstructure:"summary,omitempty"`
|
||||
|
||||
// e.g. http://www.computer.org.example.com/csdl/mags/co/1996/10/rx069-abs.html
|
||||
Url *string `json:"url,omitempty" yaml:"url,omitempty" mapstructure:"url,omitempty"`
|
||||
}
|
||||
|
||||
type ResumeReferencesElem struct {
|
||||
// e.g. Timothy Cook
|
||||
Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"`
|
||||
|
||||
// e.g. Joe blogs was a great employee, who turned up to work at least once a
|
||||
// week. He exceeded my expectations when it came to doing nothing.
|
||||
Reference *string `json:"reference,omitempty" yaml:"reference,omitempty" mapstructure:"reference,omitempty"`
|
||||
}
|
||||
|
||||
type ResumeSkillsElem struct {
|
||||
// List some keywords pertaining to this skill
|
||||
Keywords []string `json:"keywords,omitempty" yaml:"keywords,omitempty" mapstructure:"keywords,omitempty"`
|
||||
|
||||
// e.g. Master
|
||||
Level *string `json:"level,omitempty" yaml:"level,omitempty" mapstructure:"level,omitempty"`
|
||||
|
||||
// e.g. Web Development
|
||||
Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"`
|
||||
}
|
||||
|
||||
type ResumeVolunteerElem struct {
|
||||
// EndDate corresponds to the JSON schema field "endDate".
|
||||
EndDate *Iso8601 `json:"endDate,omitempty" yaml:"endDate,omitempty" mapstructure:"endDate,omitempty"`
|
||||
|
||||
// Specify accomplishments and achievements
|
||||
Highlights []string `json:"highlights,omitempty" yaml:"highlights,omitempty" mapstructure:"highlights,omitempty"`
|
||||
|
||||
// e.g. Facebook
|
||||
Organization *string `json:"organization,omitempty" yaml:"organization,omitempty" mapstructure:"organization,omitempty"`
|
||||
|
||||
// e.g. Software Engineer
|
||||
Position *string `json:"position,omitempty" yaml:"position,omitempty" mapstructure:"position,omitempty"`
|
||||
|
||||
// StartDate corresponds to the JSON schema field "startDate".
|
||||
StartDate *Iso8601 `json:"startDate,omitempty" yaml:"startDate,omitempty" mapstructure:"startDate,omitempty"`
|
||||
|
||||
// Give an overview of your responsibilities at the company
|
||||
Summary *string `json:"summary,omitempty" yaml:"summary,omitempty" mapstructure:"summary,omitempty"`
|
||||
|
||||
// e.g. http://facebook.example.com
|
||||
Url *string `json:"url,omitempty" yaml:"url,omitempty" mapstructure:"url,omitempty"`
|
||||
}
|
||||
|
||||
type ResumeWorkElem struct {
|
||||
// e.g. Social Media Company
|
||||
Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"`
|
||||
|
||||
// EndDate corresponds to the JSON schema field "endDate".
|
||||
EndDate *Iso8601 `json:"endDate,omitempty" yaml:"endDate,omitempty" mapstructure:"endDate,omitempty"`
|
||||
|
||||
// Specify multiple accomplishments
|
||||
Highlights []string `json:"highlights,omitempty" yaml:"highlights,omitempty" mapstructure:"highlights,omitempty"`
|
||||
|
||||
// e.g. Menlo Park, CA
|
||||
Location *string `json:"location,omitempty" yaml:"location,omitempty" mapstructure:"location,omitempty"`
|
||||
|
||||
// e.g. Facebook
|
||||
Name *string `json:"name,omitempty" yaml:"name,omitempty" mapstructure:"name,omitempty"`
|
||||
|
||||
// e.g. Software Engineer
|
||||
Position *string `json:"position,omitempty" yaml:"position,omitempty" mapstructure:"position,omitempty"`
|
||||
|
||||
// StartDate corresponds to the JSON schema field "startDate".
|
||||
StartDate *Iso8601 `json:"startDate,omitempty" yaml:"startDate,omitempty" mapstructure:"startDate,omitempty"`
|
||||
|
||||
// Give an overview of your responsibilities at the company
|
||||
Summary *string `json:"summary,omitempty" yaml:"summary,omitempty" mapstructure:"summary,omitempty"`
|
||||
|
||||
// e.g. http://facebook.example.com
|
||||
Url *string `json:"url,omitempty" yaml:"url,omitempty" mapstructure:"url,omitempty"`
|
||||
}
|
||||
19
internal/repo/repos.go
Normal file
19
internal/repo/repos.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/domain"
|
||||
)
|
||||
|
||||
type Resumes interface {
|
||||
Get() domain.Resume
|
||||
}
|
||||
|
||||
type Repos struct {
|
||||
Resumes
|
||||
}
|
||||
|
||||
func NewRepos(db domain.Resume) *Repos {
|
||||
return &Repos{
|
||||
Resumes: NewResumesRepo(db),
|
||||
}
|
||||
}
|
||||
19
internal/repo/resumes.go
Normal file
19
internal/repo/resumes.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/domain"
|
||||
)
|
||||
|
||||
type ResumesRepo struct {
|
||||
db domain.Resume
|
||||
}
|
||||
|
||||
func NewResumesRepo(db domain.Resume) *ResumesRepo {
|
||||
return &ResumesRepo{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ResumesRepo) Get() domain.Resume {
|
||||
return r.db
|
||||
}
|
||||
32
internal/server/server.go
Normal file
32
internal/server/server.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/config"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
}
|
||||
|
||||
func NewServer(cfg *config.Config, handler http.Handler) *Server {
|
||||
return &Server{
|
||||
httpServer: &http.Server{
|
||||
Addr: ":" + cfg.HTTP.Port,
|
||||
Handler: handler,
|
||||
ReadTimeout: cfg.HTTP.ReadTimeout,
|
||||
WriteTimeout: cfg.HTTP.WriteTimeout,
|
||||
MaxHeaderBytes: cfg.HTTP.MaxHeaderMegabytes << 20,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Run() error {
|
||||
return s.httpServer.ListenAndServe()
|
||||
}
|
||||
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
return s.httpServer.Shutdown(ctx)
|
||||
}
|
||||
20
internal/service/resume.go
Normal file
20
internal/service/resume.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/domain"
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/repo"
|
||||
)
|
||||
|
||||
type ResumesService struct {
|
||||
repo repo.Resumes
|
||||
}
|
||||
|
||||
func NewResumesService(repo repo.Resumes) *ResumesService {
|
||||
return &ResumesService{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ResumesService) Get() domain.Resume {
|
||||
return r.repo.Get()
|
||||
}
|
||||
27
internal/service/service.go
Normal file
27
internal/service/service.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/config"
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/domain"
|
||||
"git.roboces.dev/catalin/cvvvvv/internal/repo"
|
||||
)
|
||||
|
||||
type Services struct {
|
||||
Resumes Resumes
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
type Deps struct {
|
||||
Repos *repo.Repos
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
func NewServices(deps Deps) *Services {
|
||||
resumeService := NewResumesService(deps.Repos.Resumes)
|
||||
|
||||
return &Services{Resumes: resumeService, Config: deps.Config}
|
||||
}
|
||||
|
||||
type Resumes interface {
|
||||
Get() domain.Resume
|
||||
}
|
||||
100
pkg/limiter/limiter.go
Normal file
100
pkg/limiter/limiter.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
package limiter
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.roboces.dev/catalin/cvvvvv/pkg/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// visitor holds limiter and lastSeen for specific user.
|
||||
type visitor struct {
|
||||
limiter *rate.Limiter
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
// rateLimiter used to rate limit an incoming requests.
|
||||
type rateLimiter struct {
|
||||
sync.RWMutex
|
||||
|
||||
visitors map[string]*visitor
|
||||
limit rate.Limit
|
||||
burst int
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
// newRateLimiter creates an instance of the rateLimiter.
|
||||
func newRateLimiter(rps, burst int, ttl time.Duration) *rateLimiter {
|
||||
return &rateLimiter{
|
||||
visitors: make(map[string]*visitor),
|
||||
limit: rate.Limit(rps),
|
||||
burst: burst,
|
||||
ttl: ttl,
|
||||
}
|
||||
}
|
||||
|
||||
// getVisitor returns limiter for the specific visitor by its IP,
|
||||
// looking up within the visitors map.
|
||||
func (l *rateLimiter) getVisitor(ip string) *rate.Limiter {
|
||||
l.RLock()
|
||||
v, exists := l.visitors[ip]
|
||||
l.RUnlock()
|
||||
|
||||
if !exists {
|
||||
limiter := rate.NewLimiter(l.limit, l.burst)
|
||||
l.Lock()
|
||||
l.visitors[ip] = &visitor{limiter, time.Now()}
|
||||
l.Unlock()
|
||||
|
||||
return limiter
|
||||
}
|
||||
|
||||
v.lastSeen = time.Now()
|
||||
|
||||
return v.limiter
|
||||
}
|
||||
|
||||
// cleanupVisitors removes old entries from the visitors map.
|
||||
func (l *rateLimiter) cleanupVisitors() {
|
||||
for {
|
||||
time.Sleep(time.Minute)
|
||||
|
||||
l.Lock()
|
||||
for ip, v := range l.visitors {
|
||||
if time.Since(v.lastSeen) > l.ttl {
|
||||
delete(l.visitors, ip)
|
||||
}
|
||||
}
|
||||
l.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Limit creates a new rate limiter middleware handler.
|
||||
func Limit(rps int, burst int, ttl time.Duration) gin.HandlerFunc {
|
||||
l := newRateLimiter(rps, burst, ttl)
|
||||
|
||||
// run a background worker to clean up old entries
|
||||
go l.cleanupVisitors()
|
||||
|
||||
return func(c *gin.Context) {
|
||||
ip, _, err := net.SplitHostPort(c.Request.RemoteAddr)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !l.getVisitor(ip).Allow() {
|
||||
c.AbortWithStatus(http.StatusTooManyRequests)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
8
pkg/logger/logger.go
Normal file
8
pkg/logger/logger.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package logger
|
||||
|
||||
type Logger interface {
|
||||
Debug(msg string, params map[string]interface{})
|
||||
Info(msg string, params map[string]interface{})
|
||||
Warn(msg string, params map[string]interface{})
|
||||
Error(msg string, params map[string]interface{})
|
||||
}
|
||||
35
pkg/logger/logrus.go
Normal file
35
pkg/logger/logrus.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package logger
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
func Debug(msg ...interface{}) {
|
||||
logrus.Debug(msg...)
|
||||
}
|
||||
|
||||
func Debugf(format string, args ...interface{}) {
|
||||
logrus.Debugf(format, args...)
|
||||
}
|
||||
|
||||
func Info(msg ...interface{}) {
|
||||
logrus.Info(msg...)
|
||||
}
|
||||
|
||||
func Infof(format string, args ...interface{}) {
|
||||
logrus.Infof(format, args...)
|
||||
}
|
||||
|
||||
func Warn(msg ...interface{}) {
|
||||
logrus.Warn(msg...)
|
||||
}
|
||||
|
||||
func Warnf(format string, args ...interface{}) {
|
||||
logrus.Warnf(format, args...)
|
||||
}
|
||||
|
||||
func Error(msg ...interface{}) {
|
||||
logrus.Error(msg...)
|
||||
}
|
||||
|
||||
func Errorf(format string, args ...interface{}) {
|
||||
logrus.Errorf(format, args...)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue