diff --git a/Makefile b/Makefile index ea90040..7dac2be 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ buildNodeFrontend: @cd static && rm build/static/**/*.map embedFrontend: - @cd handlers/tmpls && esc -o tmpls.go -pkg tmpls -include ^*\.tmpl . + @cd handlers/tmpls && esc -o tmpls.go -pkg tmpls -include ^*\.html . @cd handlers && esc -o static.go -pkg handlers -prefix ../static/build ../static/build bash build/info.sh diff --git a/handlers/auth.go b/handlers/auth.go index 18f2d0b..da671ee 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -33,7 +33,7 @@ func (h *Handler) initOAuth() { h.providers = append(h.providers, "microsoft") } - h.engine.POST("/api/v1/check", h.handleAuthCheck) + h.engine.POST("/api/v1/auth/check", h.handleAuthCheck) } func (h *Handler) parseJWT(wt string) (*auth.JWTClaims, error) { diff --git a/handlers/auth/auth.go b/handlers/auth/auth.go index 4b4eea9..fd7979d 100644 --- a/handlers/auth/auth.go +++ b/handlers/auth/auth.go @@ -88,7 +88,7 @@ func (a *AdapterWrapper) HandleCallback(c *gin.Context) { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.HTML(http.StatusOK, "token.tmpl", gin.H{ + c.HTML(http.StatusOK, "token.html", gin.H{ "token": token, }) } diff --git a/handlers/auth_test.go b/handlers/auth_test.go index b56cb24..23303b0 100644 --- a/handlers/auth_test.go +++ b/handlers/auth_test.go @@ -99,7 +99,7 @@ func TestCheckToken(t *testing.T) { if err != nil { t.Fatalf("could not post to the backend: %v", err) } - resp, err := http.Post(server.URL+"/api/v1/check", "application/json", bytes.NewBuffer(body)) + resp, err := http.Post(server.URL+"/api/v1/auth/check", "application/json", bytes.NewBuffer(body)) if err != nil { t.Fatalf("could not execute get request: %v", err) } diff --git a/handlers/handlers.go b/handlers/handlers.go index fddda81..5399dad 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -62,8 +62,11 @@ func (h *Handler) setTemplateFromFS(name string) error { } func (h *Handler) setHandlers() error { - if err := h.setTemplateFromFS("token.tmpl"); err != nil { - return errors.Wrap(err, "could not set template from FS") + templates := []string{"token.html", "protected.html"} + for _, template := range templates { + if err := h.setTemplateFromFS(template); err != nil { + return errors.Wrapf(err, "could not set template %s from FS", template) + } } h.engine.Use(ginrus.Ginrus(logrus.StandardLogger(), time.RFC3339, false)) protected := h.engine.Group("/api/v1/protected") diff --git a/handlers/public.go b/handlers/public.go index cc2dec6..e9fd73a 100644 --- a/handlers/public.go +++ b/handlers/public.go @@ -14,14 +14,15 @@ import ( "github.com/maxibanki/golang-url-shortener/handlers/auth" "github.com/maxibanki/golang-url-shortener/store" "github.com/maxibanki/golang-url-shortener/util" + "golang.org/x/crypto/bcrypt" ) -// urlUtil is used to help in- and outgoing requests for json +// requestHelper is used to help in- and outgoing requests for json // un- and marshalling -type urlUtil struct { - URL string `binding:"required"` - ID, DeletionURL string - Expiration *time.Time +type requestHelper struct { + URL string `binding:"required"` + ID, DeletionURL, Password string + Expiration *time.Time } // handleLookup is the http handler for getting the infos @@ -52,7 +53,7 @@ func (h *Handler) handleLookup(c *gin.Context) { // handleAccess handles the access for incoming requests func (h *Handler) handleAccess(c *gin.Context) { id := c.Request.URL.Path[1:] - url, err := h.store.GetURLAndIncrease(id) + entry, err := h.store.GetEntryAndIncrease(id) if err == store.ErrNoEntryFound { return } else if err != nil { @@ -70,7 +71,31 @@ func (h *Handler) handleAccess(c *gin.Context) { UTMContent: c.Query("utm_content"), UTMTerm: c.Query("utm_term"), }) - c.Redirect(http.StatusTemporaryRedirect, url) + // No password set + if len(entry.Password) == 0 { + c.Redirect(http.StatusTemporaryRedirect, entry.Public.URL) + } else { + templateError := "" + if c.Request.Method == "POST" { + pw, exists := c.GetPostForm("password") + if exists { + if err := bcrypt.CompareHashAndPassword(entry.Password, []byte(pw)); err != nil { + templateError = fmt.Sprintf("could not validate password: %v", err) + } + } else { + templateError = "No password set" + } + if templateError == "" { + c.Redirect(http.StatusTemporaryRedirect, entry.Public.URL) + c.Abort() + return + } + } + c.HTML(http.StatusOK, "protected.html", gin.H{ + "ID": id, + "Error": templateError, + }) + } // There is a need to Abort in the current middleware to prevent // that the status code will be overridden by the default NoRoute handler c.Abort() @@ -78,7 +103,7 @@ func (h *Handler) handleAccess(c *gin.Context) { // handleCreate handles requests to create an entry func (h *Handler) handleCreate(c *gin.Context) { - var data urlUtil + var data requestHelper if err := c.ShouldBind(&data); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return @@ -92,13 +117,13 @@ func (h *Handler) handleCreate(c *gin.Context) { RemoteAddr: c.ClientIP(), OAuthProvider: user.OAuthProvider, OAuthID: user.OAuthID, - }, data.ID) + }, data.ID, data.Password) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } originURL := h.getURLOrigin(c) - c.JSON(http.StatusOK, urlUtil{ + c.JSON(http.StatusOK, requestHelper{ URL: fmt.Sprintf("%s/%s", originURL, id), DeletionURL: fmt.Sprintf("%s/d/%s/%s", originURL, id, url.QueryEscape(base64.RawURLEncoding.EncodeToString(delID))), }) diff --git a/handlers/public_test.go b/handlers/public_test.go index 1d3a72e..a54be73 100644 --- a/handlers/public_test.go +++ b/handlers/public_test.go @@ -25,7 +25,7 @@ func TestCreateEntry(t *testing.T) { ignoreResponse bool contentType string response gin.H - requestBody urlUtil + requestBody requestHelper statusCode int }{ { @@ -37,7 +37,7 @@ func TestCreateEntry(t *testing.T) { }, { name: "short URL generation", - requestBody: urlUtil{ + requestBody: requestHelper{ URL: "https://www.google.de/", }, statusCode: http.StatusOK, @@ -45,7 +45,7 @@ func TestCreateEntry(t *testing.T) { }, { name: "no valid URL", - requestBody: urlUtil{ + requestBody: requestHelper{ URL: "this is really not a URL", }, statusCode: http.StatusBadRequest, @@ -76,7 +76,7 @@ func TestCreateEntry(t *testing.T) { if tc.ignoreResponse { return } - var parsed urlUtil + var parsed requestHelper if err := json.Unmarshal(respBody, &parsed); err != nil { t.Fatalf("could not unmarshal data: %v", err) } @@ -96,7 +96,7 @@ func TestHandleInfo(t *testing.T) { t.Fatalf("could not marshal json: %v", err) } respBody := createEntryWithJSON(t, reqBody, "application/json; charset=utf-8", http.StatusOK) - var parsed urlUtil + var parsed requestHelper if err = json.Unmarshal(respBody, &parsed); err != nil { t.Fatalf("could not unmarshal data: %v", err) } @@ -257,7 +257,7 @@ func TestHandleDeletion(t *testing.T) { t.Fatalf("could not marshal json: %v", err) } respBody := createEntryWithJSON(t, reqBody, "application/json; charset=utf-8", http.StatusOK) - var body urlUtil + var body requestHelper if err := json.Unmarshal(respBody, &body); err != nil { t.Fatal("could not unmarshal create response") } diff --git a/handlers/tmpls/protected.html b/handlers/tmpls/protected.html new file mode 100644 index 0000000..f24e8c7 --- /dev/null +++ b/handlers/tmpls/protected.html @@ -0,0 +1,68 @@ + + + + + + + + Authorize please + + + + + +
+
+

+ +
+ Visit the URL with the ID {{ .ID }} +
+

+
+
+
+
+ + +
+
+ +
+
+ {{ if .Error }} +
+ +
+ Authorization error occured +
+

{{ .Error }}

+
+ {{ end }} +
+ New to us and want to create an own shortened URL? + Sign Up +
+
+
+ + + + \ No newline at end of file diff --git a/handlers/tmpls/token.tmpl b/handlers/tmpls/token.html similarity index 100% rename from handlers/tmpls/token.tmpl rename to handlers/tmpls/token.html diff --git a/main.go b/main.go index 93847c6..5df13ee 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,6 @@ import ( ) func main() { - os.Setenv("GUS_SHORTED_ID_LENGTH", "4") stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt) logrus.SetFormatter(&logrus.TextFormatter{ diff --git a/static/package.json b/static/package.json index 6f95eec..849f036 100644 --- a/static/package.json +++ b/static/package.json @@ -6,6 +6,10 @@ "/api": { "target": "http://127.0.0.1:8080", "ws": true + }, + "/d": { + "target": "http://127.0.0.1:8080", + "ws": true } }, "dependencies": { @@ -31,4 +35,4 @@ "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" } -} +} \ No newline at end of file diff --git a/static/src/Home/Home.css b/static/src/Home/Home.css index a33a3b6..0dd4f24 100644 --- a/static/src/Home/Home.css +++ b/static/src/Home/Home.css @@ -3,4 +3,9 @@ } .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list { padding: 0 0; +} +@media only screen and (max-width: 767px) { + .FieldsMarginButtomFix { + margin-bottom: 1rem !important; + } } \ No newline at end of file diff --git a/static/src/Home/Home.js b/static/src/Home/Home.js index ac60bab..90afec5 100644 --- a/static/src/Home/Home.js +++ b/static/src/Home/Home.js @@ -11,6 +11,7 @@ import './Home.css' export default class HomeComponent extends Component { handleURLChange = (e, { value }) => this.url = value + handlePasswordChange = (e, { value }) => this.password = value handleCustomExpirationChange = expire => this.setState({ expiration: expire }) handleCustomIDChange = (e, { value }) => { this.customID = value @@ -30,7 +31,6 @@ export default class HomeComponent extends Component { }) .catch(e => { this.setState({ showCustomIDError: false }) - toastr.error(`Could not fetch lookup: ${e}`) }) } onSettingsChange = (e, { value }) => this.setState({ setOptions: value }) @@ -39,11 +39,12 @@ export default class HomeComponent extends Component { links: [], options: [ { text: 'Custom URL', value: 'custom' }, - { text: 'Expiration', value: 'expire' } + { text: 'Expiration', value: 'expire' }, + { text: 'Password', value: 'protected' } ], setOptions: [], showCustomIDError: false, - expiration: moment() + expiration: null } componentDidMount() { this.urlInput.focus() @@ -55,7 +56,8 @@ export default class HomeComponent extends Component { body: JSON.stringify({ URL: this.url, ID: this.customID, - Expiration: this.state.setOptions.indexOf("expire") > -1 ? this.state.expiration.toISOString() : undefined + Expiration: this.state.setOptions.includes("expire") && this.state.expiration ? this.state.expiration.toISOString() : undefined, + Password: this.state.setOptions.includes("protected") && this.password ? this.password : undefined }), headers: { 'Authorization': window.localStorage.getItem('token'), @@ -67,11 +69,11 @@ export default class HomeComponent extends Component { links: [...this.state.links, [ r.URL, this.url, - this.state.setOptions.indexOf("expire") > -1 ? this.state.expiration.toISOString() : undefined, + this.state.setOptions.includes("expire") && this.state.expiration ? this.state.expiration.toISOString() : undefined, r.DeletionURL ]] })) - .catch(e => toastr.error(`Could not fetch create: ${e}`)) + .catch(e => e instanceof Promise ? e.then(error => toastr.error(`Could not fetch lookup: ${error.error}`)) : toastr.error(`Could not fetch create: ${e}`)) } } @@ -96,16 +98,25 @@ export default class HomeComponent extends Component { } - {setOptions.indexOf("expire") > -1 && + + + {setOptions.includes("expire") && } minDate={moment()} /> + placeholderText="Click to select a date" + dateFormat="LLL" + onChange={this.handleCustomExpirationChange} + selected={expiration} + customInput={} + minDate={moment()} /> } + {setOptions.includes("protected") && + } diff --git a/static/src/index.js b/static/src/index.js index ecd1ed0..9dd8806 100644 --- a/static/src/index.js +++ b/static/src/index.js @@ -40,7 +40,7 @@ export default class BaseComponent extends Component { const that = this, token = window.localStorage.getItem('token'); if (token) { - fetch('/api/v1/check', { + fetch('/api/v1/auth/check', { method: 'POST', body: JSON.stringify({ Token: token @@ -106,18 +106,18 @@ export default class BaseComponent extends Component {

The following authentication services are currently available:

{info &&
{info.providers.length === 0 &&

There are currently no correct oAuth credentials maintained.

} - {info.providers.indexOf("google") !== -1 &&
+ {info.providers.includes("google") &&
- {info.providers.indexOf("github") !== -1 &&
} + {info.providers.includes("github") &&
}
} - {info.providers.indexOf("github") !== -1 &&
+ {info.providers.includes("github") &&
} - {info.providers.indexOf("microsoft") !== -1 &&
+ {info.providers.includes("microsoft") &&