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
|
# Ignore everything
|
||||||
# Byte-compiled / optimized / DLL files
|
*
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
# But don't ignore these files...
|
||||||
*.so
|
!/.gitignore
|
||||||
|
|
||||||
# Distribution / packaging
|
!*.go
|
||||||
.Python
|
!go.sum
|
||||||
build/
|
!go.mod
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
!README.md
|
||||||
# Usually these files are written by a python script from a template
|
!LICENSE
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
!apischema/openapi.yaml
|
||||||
*.manifest
|
!apischema/resumeschema.json
|
||||||
*.spec
|
!configs/config.yml
|
||||||
|
!configs/example.db.json
|
||||||
|
|
||||||
# Installer logs
|
# !Makefile
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
# ...even if they are in subdirectories
|
||||||
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~
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
# cvvvv
|
# 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