Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add i18n framework #55

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
deploy/
dist/
.vscode/

artifact_go.sh
*.exe
16 changes: 14 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
# Development
FROM golang:1.9.2-alpine AS development
FROM golang:1.10.2-alpine AS development

WORKDIR /go/src/github.com/tidepool-org/hydrophone

COPY . .
RUN apk --no-cache update && \
apk --no-cache upgrade && \
apk add build-base git cyrus-sasl-dev rsync

RUN ./build.sh
RUN dos2unix build.sh && ./build.sh && \
dos2unix test.sh && \
dos2unix env.sh && \
dos2unix artifact.sh && \
dos2unix start.sh && \
dos2unix version.sh

CMD ["./dist/hydrophone"]

Expand All @@ -15,12 +23,16 @@ FROM alpine:latest AS release
RUN apk --no-cache update && \
apk --no-cache upgrade && \
apk add --no-cache ca-certificates && \
apk add --no-cache libsasl && \
adduser -D tidepool

WORKDIR /home/tidepool

USER tidepool

COPY --from=development --chown=tidepool /go/src/github.com/tidepool-org/hydrophone/dist/hydrophone .
COPY --chown=tidepool templates/html ./templates/html/
COPY --chown=tidepool templates/locales ./templates/locales/
COPY --chown=tidepool templates/meta ./templates/meta/

CMD ["./hydrophone"]
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,53 @@ hydrophone

[![Build Status](https://travis-ci.org/tidepool-org/hydrophone.png)](https://travis-ci.org/tidepool-org/hydrophone)

This API sends notifications to users for things like forgotten passwords, initial signup, and invitations.
This API sends notifications (using relevant language) to users for things like forgotten passwords, initial signup, and invitations.

## Building
To build the Hydrophone module you simply need to execute the build script:

```
$ ./build.sh
```
This will automatically get the dependencies (using goget) and build the code.


## Running the Tests
If you would like to contribute then you will likely need to run the tests locally before pushing your changes.
To run **all** tests you can simply execute the test script from your favorite shell:

`$ ./test.sh`

To run the tests for a particular folder (i.e. the api part) you need to go into this folder and execute the gotest command:
To run all tests for this repo then in the root directory use:

```
$ cd ./api
$ gotest
```


## Config
The configuration is provided to Hydrophone via 2 environment variables: `TIDEPOOL_HYDROPHONE_ENV` and `TIDEPOOL_HYDROPHONE_SERVICE`.
The script `env.sh` provided in this repo will set all the necessary variables with default values, allowing you to work on your development environment. However when deploying on another environment, or when using docker you will likely need to change these variables to match your setup.

## Notes on email customization and internationalization
More information on this in [docs/README.md](docs/README.md)

The emails sent by Hydrophone can be customized and translated in the user's language.
The templates and locales files are located under /templates:
* /templates/html: html template files
* /templates/locales: content in various languages
* /templates/meta: email structure

**Configuration note:** you do need to provide the template location to Hydrophone in the environment variables as an absolute path. relative path won't work.
For example:
```
export TIDEPOOL_HYDROPHONE_SERVICE='{
"hydrophone" : {
...
"i18nTemplatesPath": "/var/data/hydrophone/templates"
},
...
}'
```
23 changes: 22 additions & 1 deletion api/forgot.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ type (
// status: 200
// status: 400 no email given
func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map[string]string) {
// By default, the reseter language will be his browser's or "en" for Englih
// In case the reseter is found a known user and has a language set, the language will be overriden in a later step
var reseterLanguage string
if reseterLanguage = getBrowserPreferredLanguage(req); reseterLanguage == "" {
reseterLanguage = "en"
}

email := vars["useremail"]
if email == "" {
Expand All @@ -51,8 +57,23 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map
resetCnf, _ := models.NewConfirmation(models.TypePasswordReset, models.TemplateNamePasswordReset, "")
resetCnf.Email = email

// if the reseter is already a Tidepool user, we can use his preferences
if resetUsr := a.findExistingUser(resetCnf.Email, a.sl.TokenProvide()); resetUsr != nil {
resetCnf.UserId = resetUsr.UserID

// let's get the reseter user preferences
reseterPreferences := &models.Preferences{}
if err := a.seagull.GetCollection(resetCnf.UserId, "preferences", a.sl.TokenProvide(), reseterPreferences); err != nil {
a.sendError(res, http.StatusInternalServerError,
STATUS_ERR_FINDING_USR,
"forgot password: error getting reseter user preferences: ",
err.Error())
return
}
// if reseter has a profile and a language we override the previously set language (browser's or "en")
if reseterPreferences.DisplayLanguage != "" {
reseterLanguage = reseterPreferences.DisplayLanguage
}
} else {
log.Print(STATUS_RESET_NO_ACCOUNT)
log.Printf("email used [%s]", email)
Expand All @@ -70,7 +91,7 @@ func (a *Api) passwordReset(res http.ResponseWriter, req *http.Request, vars map
"Email": resetCnf.Email,
}

if a.createAndSendNotification(resetCnf, emailContent) {
if a.createAndSendNotification(resetCnf, emailContent, reseterLanguage) {
a.logMetricAsServer("reset confirmation sent")
} else {
a.logMetricAsServer("reset confirmation failed to be sent")
Expand Down
90 changes: 69 additions & 21 deletions api/hydrophoneApi.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

"github.com/gorilla/mux"
"github.com/nicksnyder/go-i18n/v2/i18n"

commonClients "github.com/tidepool-org/go-common/clients"
"github.com/tidepool-org/go-common/clients/highwater"
Expand All @@ -21,19 +22,21 @@ import (

type (
Api struct {
Store clients.StoreClient
notifier clients.Notifier
templates models.Templates
sl shoreline.Client
gatekeeper commonClients.Gatekeeper
seagull commonClients.Seagull
metrics highwater.Client
Config Config
Store clients.StoreClient
notifier clients.Notifier
templates models.Templates
sl shoreline.Client
gatekeeper commonClients.Gatekeeper
seagull commonClients.Seagull
metrics highwater.Client
Config Config
LanguageBundle *i18n.Bundle
}
Config struct {
ServerSecret string `json:"serverSecret"` //used for services
WebURL string `json:"webUrl"`
AssetURL string `json:"assetUrl"`
ServerSecret string `json:"serverSecret"` //used for services
WebURL string `json:"webUrl"`
AssetURL string `json:"assetUrl"`
I18nTemplatesPath string `json:"i18nTemplatesPath"`
}

group struct {
Expand Down Expand Up @@ -74,17 +77,45 @@ func InitApi(
templates models.Templates,
) *Api {
return &Api{
Store: store,
Config: cfg,
notifier: ntf,
sl: sl,
gatekeeper: gatekeeper,
metrics: metrics,
seagull: seagull,
templates: templates,
Store: store,
Config: cfg,
notifier: ntf,
sl: sl,
gatekeeper: gatekeeper,
metrics: metrics,
seagull: seagull,
templates: templates,
LanguageBundle: nil,
}
}

// InitApiI18n initializes both the API and the i18n artefacts
func InitApiWithI18n(
cfg Config,
store clients.StoreClient,
ntf clients.Notifier,
sl shoreline.Client,
gatekeeper commonClients.Gatekeeper,
metrics highwater.Client,
seagull commonClients.Seagull,
templates models.Templates,
) *Api {
var theAPI *Api
theAPI = &Api{
Store: store,
Config: cfg,
notifier: ntf,
sl: sl,
gatekeeper: gatekeeper,
metrics: metrics,
seagull: seagull,
templates: templates,
LanguageBundle: nil,
}
theAPI.InitI18n(cfg.I18nTemplatesPath)
return theAPI
}

func (a *Api) SetHandlers(prefix string, rtr *mux.Router) {

rtr.HandleFunc("/status", a.GetStatus).Methods("GET")
Expand Down Expand Up @@ -210,7 +241,11 @@ func (a *Api) checkFoundConfirmations(res http.ResponseWriter, results []*models
}

//Generate a notification from the given confirmation,write the error if it fails
func (a *Api) createAndSendNotification(conf *models.Confirmation, content map[string]interface{}) bool {
func (a *Api) createAndSendNotification(conf *models.Confirmation, content map[string]interface{}, lang string) bool {

log.Printf("sending notification with template %s to %s with language %s", conf.TemplateName, conf.Email, lang)

// Get the template name based on the requested communication type
templateName := conf.TemplateName
if templateName == models.TemplateNameUndefined {
switch conf.Type {
Expand All @@ -228,21 +263,32 @@ func (a *Api) createAndSendNotification(conf *models.Confirmation, content map[s
}
}

// Content collection is here to replace placeholders in template body/content
content["WebURL"] = a.Config.WebURL
content["AssetURL"] = a.Config.AssetURL

// Retrieve the template from all the preloaded templates
template, ok := a.templates[templateName]
if !ok {
log.Printf("Unknown template type %s", templateName)
return false
}

subject, body, err := template.Execute(content)
// Add dynamic content to the template
fillTemplate(template, a.LanguageBundle, lang, content)

// Email information (subject and body) are retrieved from the "executed" email template
// "Execution" adds dynamic content using text/template lib
_, body, err := template.Execute(content)

if err != nil {
log.Printf("Error executing email template %s", err)
return false
}
// Get localized subject of email
subject, err := getLocalizedSubject(a.LanguageBundle, template.Subject(), lang)

// Finally send the email
if status, details := a.notifier.Send([]string{conf.Email}, subject, body); status != http.StatusOK {
log.Printf("Issue sending email: Status [%d] Message [%s]", status, details)
return false
Expand All @@ -253,6 +299,7 @@ func (a *Api) createAndSendNotification(conf *models.Confirmation, content map[s
//find and validate the token
func (a *Api) token(res http.ResponseWriter, req *http.Request) *shoreline.TokenData {
if token := req.Header.Get(TP_SESSION_TOKEN); token != "" {
log.Printf("Found token in request header %s", token)
td := a.sl.CheckToken(token)

if td == nil {
Expand Down Expand Up @@ -293,6 +340,7 @@ func (a *Api) findExistingUser(indentifier, token string) *shoreline.UserData {
log.Printf("Error [%s] trying to get existing users details", err.Error())
return nil
} else {
log.Printf("User found at shoreline using token %s", token)
return usr
}
}
Expand Down
Loading