wip: add api auth and /resume endpoint
This commit is contained in:
parent
22e2f6005f
commit
2c6d063717
28 changed files with 2486 additions and 271 deletions
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)
|
||||
}
|
||||
}
|
||||
140
internal/config/config.go
Normal file
140
internal/config/config.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHTTPPort = "8000"
|
||||
defaultHTTPRWTimeout = 10 * time.Second
|
||||
defaultHTTPMaxHeaderMegabytes = 1
|
||||
defaultAccessTokenTTL = 15 * time.Minute
|
||||
defaultRefreshTokenTTL = 24 * time.Hour * 30
|
||||
defaultLimiterRPS = 10
|
||||
defaultLimiterBurst = 2
|
||||
defaultLimiterTTL = 10 * time.Minute
|
||||
defaultVerificationCodeLength = 8
|
||||
|
||||
EnvLocal = "local"
|
||||
Prod = "prod"
|
||||
)
|
||||
|
||||
type (
|
||||
Config struct {
|
||||
Environment string
|
||||
HTTP HTTPConfig
|
||||
Auth AuthConfig
|
||||
Database string `mapstructure:"database"`
|
||||
Limiter LimiterConfig
|
||||
CacheTTL time.Duration `mapstructure:"ttl"`
|
||||
}
|
||||
|
||||
AuthConfig struct {
|
||||
ApiKeys map[string]string `mapstructure:"api_keys"`
|
||||
}
|
||||
|
||||
HTTPConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port string `mapstructure:"port"`
|
||||
ReadTimeout time.Duration `mapstructure:"readTimeout"`
|
||||
WriteTimeout time.Duration `mapstructure:"writeTimeout"`
|
||||
MaxHeaderMegabytes int `mapstructure:"maxHeaderBytes"`
|
||||
}
|
||||
|
||||
LimiterConfig struct {
|
||||
RPS int
|
||||
Burst int
|
||||
TTL time.Duration
|
||||
}
|
||||
)
|
||||
|
||||
// Init populates Config struct with values from config file
|
||||
// located at filepath and environment variables.
|
||||
func Init(configsDir string) (*Config, error) {
|
||||
populateDefaults()
|
||||
|
||||
if err := parseConfigFile(configsDir, os.Getenv("APP_ENV")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := unmarshal(&cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//setFromEnv(&cfg)
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func unmarshal(cfg *Config) error {
|
||||
if err := viper.UnmarshalKey("environment", &cfg.Environment); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := viper.UnmarshalKey("cache.ttl", &cfg.CacheTTL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.UnmarshalKey("http", &cfg.HTTP); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.UnmarshalKey("auth", &cfg.Auth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
database := viper.GetString("database")
|
||||
configDir := filepath.Dir(viper.ConfigFileUsed())
|
||||
databaseAbsPath := filepath.Join(configDir, database)
|
||||
viper.Set("database", databaseAbsPath)
|
||||
|
||||
if err := viper.UnmarshalKey("database", &cfg.Database); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return viper.UnmarshalKey("limiter", &cfg.Limiter)
|
||||
|
||||
}
|
||||
|
||||
func setFromEnv(cfg *Config) {
|
||||
err := envconfig.Process("cvvvvv", &cfg)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func parseConfigFile(folder, env string) error {
|
||||
viper.AddConfigPath(folder)
|
||||
viper.SetConfigName("config")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if env == EnvLocal {
|
||||
return nil
|
||||
}
|
||||
|
||||
viper.SetConfigName(env)
|
||||
|
||||
return viper.MergeInConfig()
|
||||
}
|
||||
|
||||
func populateDefaults() {
|
||||
viper.SetDefault("http.port", defaultHTTPPort)
|
||||
viper.SetDefault("http.max_header_megabytes", defaultHTTPMaxHeaderMegabytes)
|
||||
viper.SetDefault("http.timeouts.read", defaultHTTPRWTimeout)
|
||||
viper.SetDefault("http.timeouts.write", defaultHTTPRWTimeout)
|
||||
viper.SetDefault("auth.accessTokenTTL", defaultAccessTokenTTL)
|
||||
viper.SetDefault("auth.refreshTokenTTL", defaultRefreshTokenTTL)
|
||||
viper.SetDefault("auth.verificationCodeLength", defaultVerificationCodeLength)
|
||||
viper.SetDefault("limiter.rps", defaultLimiterRPS)
|
||||
viper.SetDefault("limiter.burst", defaultLimiterBurst)
|
||||
viper.SetDefault("limiter.ttl", defaultLimiterTTL)
|
||||
}
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue