wip: add api auth and /resume endpoint

This commit is contained in:
cătălin 2023-12-04 22:21:34 +01:00
commit 2c6d063717
Signed by: catalin
GPG key ID: 0178DF42F43E5FD2
28 changed files with 2486 additions and 271 deletions

74
internal/app/app.go Normal file
View 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
View 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)
}

View 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)
}
}

View 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)
}
}

View 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)
}
}

View 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")
}

View 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")
}

View 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})
}

View 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(),
})
}

View 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
View 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
View 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
View 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
View 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)
}

View 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()
}

View 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
}