diff --git a/configs/dev.yml b/configs/dev.yml index 14393f3..ee933ea 100644 --- a/configs/dev.yml +++ b/configs/dev.yml @@ -12,6 +12,9 @@ server: writerTimeout: 86400000 # 1 day maxHeaderBytes: 512 publicPath: "public" + captchaWidth: 256 + captchaHeight: 60 + captchaEnabled: true users: enableAuth: true defaultAdmin: "" @@ -20,4 +23,4 @@ users: cookieSecure: false cookieHttpOnly: true minUserNameLen: 2 - minPwdLen: 4 \ No newline at end of file + minPwdLen: 4 diff --git a/configs/docker.yml b/configs/docker.yml index 02d8266..780f9b6 100644 --- a/configs/docker.yml +++ b/configs/docker.yml @@ -12,6 +12,9 @@ server: writerTimeout: 86400000 # 1 day maxHeaderBytes: 512 publicPath: "/quickshare/public" + captchaWidth: 256 + captchaHeight: 60 + captchaEnabled: true users: enableAuth: true defaultAdmin: "" @@ -21,4 +24,3 @@ users: cookieHttpOnly: true minUserNameLen: 4 minPwdLen: 6 - diff --git a/go.mod b/go.mod index 564573d..017a34c 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,13 @@ module github.com/ihexxa/quickshare go 1.13 require ( + github.com/afocus/captcha v0.0.0-20191010092841-4bd1f21c8868 github.com/boltdb/bolt v1.3.1 + github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f github.com/elazarl/goproxy v0.0.0-20201021153353-00ad82a08272 // indirect github.com/gin-contrib/static v0.0.0-20200916080430-d45d9a37d28e github.com/gin-gonic/gin v1.6.3 + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/ihexxa/gocfg v0.0.0-20201206115732-ab537e3b1086 github.com/ihexxa/multipart v0.0.0-20201207132919-72f6e0e58b25 github.com/jessevdk/go-flags v1.4.0 @@ -18,6 +21,7 @@ require ( github.com/smartystreets/goconvey v1.6.4 // indirect go.uber.org/zap v1.16.0 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect moul.io/http2curl v1.0.0 // indirect diff --git a/go.sum b/go.sum index eb18174..bf7b246 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,14 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/afocus/captcha v0.0.0-20191010092841-4bd1f21c8868 h1:uFrPOl1VBt/Abfl2z+A/DFc+AwmFLxEHR1+Yq6cXvww= +github.com/afocus/captcha v0.0.0-20191010092841-4bd1f21c8868/go.mod h1:srphKZ1i+yGXxl/LpBS7ZIECTjCTPzZzAMtJWoG3sLo= github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M= +github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY= github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/elazarl/goproxy v0.0.0-20201021153353-00ad82a08272 h1:Am81SElhR3XCQBunTisljzNkNese2T1FiV8jP79+dqg= github.com/elazarl/goproxy v0.0.0-20201021153353-00ad82a08272/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= @@ -27,6 +31,8 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= @@ -105,6 +111,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -124,6 +132,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= diff --git a/public/static/css/style.css b/public/static/css/style.css index 302f115..30940a4 100644 --- a/public/static/css/style.css +++ b/public/static/css/style.css @@ -651,4 +651,11 @@ div.hr { .txt-cap { text-transform: capitalize; +} + +.captcha { + width: 14rem; + height: 3rem; + border: solid 1px #95a5a6; + border-radius: 0.5rem; } \ No newline at end of file diff --git a/src/client/web/src/client/index.ts b/src/client/web/src/client/index.ts index 52f3c34..7b2f9c5 100644 --- a/src/client/web/src/client/index.ts +++ b/src/client/web/src/client/index.ts @@ -48,7 +48,7 @@ export interface ListUploadingsResp { } export interface IUsersClient { - login: (user: string, pwd: string) => Promise; + login: (user: string, pwd: string, captchaId: string, captchaInput:string) => Promise; logout: () => Promise; isAuthed: () => Promise; self: () => Promise; @@ -60,6 +60,7 @@ export interface IUsersClient { addRole: (role: string) => Promise; delRole: (role: string) => Promise; listRoles: () => Promise; + getCaptchaID: () => Promise; } export interface IFilesClient { diff --git a/src/client/web/src/client/users.ts b/src/client/web/src/client/users.ts index c7037e9..5d427eb 100644 --- a/src/client/web/src/client/users.ts +++ b/src/client/web/src/client/users.ts @@ -5,13 +5,15 @@ export class UsersClient extends BaseClient { super(url); } - login = (user: string, pwd: string): Promise => { + login = (user: string, pwd: string, captchaId: string, captchaInput:string): Promise => { return this.do({ method: "post", url: `${this.url}/v1/users/login`, data: { user, pwd, + captchaId, + captchaInput, }, }); }; @@ -114,4 +116,12 @@ export class UsersClient extends BaseClient { params: {}, }); }; + + getCaptchaID = (): Promise => { + return this.do({ + method: "get", + url: `${this.url}/v1/captchas/`, + params: {}, + }); + }; } diff --git a/src/client/web/src/client/users_mock.ts b/src/client/web/src/client/users_mock.ts index a1b62a8..6b006b8 100644 --- a/src/client/web/src/client/users_mock.ts +++ b/src/client/web/src/client/users_mock.ts @@ -15,6 +15,7 @@ export class MockUsersClient { private delRoleMockResp: Promise; private listRolesMockResp: Promise; private selfMockResp: Promise; + private getCaptchaIDMockResp: Promise; constructor(url: string) { this.url = url; @@ -22,86 +23,93 @@ export class MockUsersClient { loginMock = (resp: Promise) => { this.loginMockResp = resp; - } + }; logoutMock = (resp: Promise) => { this.logoutMockResp = resp; - } + }; isAuthedMock = (resp: Promise) => { this.isAuthedMockResp = resp; - } + }; setPwdMock = (resp: Promise) => { this.setPwdMockResp = resp; - } + }; forceSetPwdMock = (resp: Promise) => { this.forceSetPwdMockResp = resp; - } + }; addUserMock = (resp: Promise) => { this.addUserMockResp = resp; - } + }; delUserMock = (resp: Promise) => { this.delUserMockResp = resp; - } + }; listUsersMock = (resp: Promise) => { this.listUsersMockResp = resp; - } + }; addRoleMock = (resp: Promise) => { this.addRoleMockResp = resp; - } + }; delRoleMock = (resp: Promise) => { this.delRoleMockResp = resp; - } + }; listRolesMock = (resp: Promise) => { this.listRolesMockResp = resp; - } - slefMock = (resp: Promise) => { + }; + selfMock = (resp: Promise) => { this.selfMockResp = resp; - } + }; + getCaptchaIDMock = (resp: Promise) => { + this.getCaptchaIDMockResp = resp; + }; login = (user: string, pwd: string): Promise => { return this.loginMockResp; - } + }; logout = (): Promise => { return this.logoutMockResp; - } + }; isAuthed = (): Promise => { return this.isAuthedMockResp; - } + }; setPwd = (oldPwd: string, newPwd: string): Promise => { return this.setPwdMockResp; - } - + }; + forceSetPwd = (userID: string, newPwd: string): Promise => { return this.forceSetPwdMockResp; - } + }; addUser = (name: string, pwd: string, role: string): Promise => { return this.addUserMockResp; - } + }; delUser = (userID: string): Promise => { return this.delUserMockResp; - } - + }; + listUsers = (): Promise => { return this.listUsersMockResp; - } + }; addRole = (role: string): Promise => { return this.addRoleMockResp; - } + }; delRole = (role: string): Promise => { return this.delRoleMockResp; - } + }; listRoles = (): Promise => { return this.listRolesMockResp; - } + }; self = (): Promise => { return this.selfMockResp; - } + }; + + getCaptchaID = (): Promise => { + return this.getCaptchaIDMockResp; + }; } diff --git a/src/client/web/src/components/core_state.ts b/src/client/web/src/components/core_state.ts index eae5411..24b0e78 100644 --- a/src/client/web/src/components/core_state.ts +++ b/src/client/web/src/components/core_state.ts @@ -50,6 +50,7 @@ export function initState(): ICoreState { displaying: "browser", authPane: { authed: false, + captchaID: "", }, browser: { isVertical: isVertical(), @@ -65,6 +66,7 @@ export function initState(): ICoreState { paneNames: Set(["settings", "login", "admin"]), login: { authed: false, + captchaID: "", }, admin: { users: Map(), @@ -83,6 +85,7 @@ export function mockState(): ICoreState { displaying: "browser", authPane: { authed: false, + captchaID: "", }, browser: { isVertical: false, @@ -98,6 +101,7 @@ export function mockState(): ICoreState { paneNames: Set(["settings", "login", "admin"]), login: { authed: false, + captchaID: "", }, admin: { users: Map(), diff --git a/src/client/web/src/components/pane_login.tsx b/src/client/web/src/components/pane_login.tsx index 08ab757..6bdfba0 100644 --- a/src/client/web/src/components/pane_login.tsx +++ b/src/client/web/src/components/pane_login.tsx @@ -10,6 +10,7 @@ import { Layouter } from "./layouter"; export interface Props { authed: boolean; + captchaID: string; update?: (updater: (prevState: ICoreState) => ICoreState) => void; } @@ -23,8 +24,13 @@ export class Updater { Updater.client = client; }; - static login = async (user: string, pwd: string): Promise => { - const resp = await Updater.client.login(user, pwd); + static login = async ( + user: string, + pwd: string, + captchaID: string, + captchaInput: string + ): Promise => { + const resp = await Updater.client.login(user, pwd, captchaID, captchaInput); Updater.setAuthed(resp.status === 200); return resp.status === 200; }; @@ -50,6 +56,15 @@ export class Updater { Updater.props.authed = isAuthed; }; + static getCaptchaID = async (): Promise => { + return Updater.client.getCaptchaID().then((resp) => { + if (resp.status === 200) { + Updater.props.captchaID = resp.data.id; + } + return resp.status === 200; + }); + }; + static setAuthPane = (preState: ICoreState): ICoreState => { preState.panel.authPane = { ...preState.panel.authPane, @@ -62,6 +77,7 @@ export class Updater { export interface State { user: string; pwd: string; + captchaInput: string; } export class AuthPane extends React.Component { @@ -74,6 +90,7 @@ export class AuthPane extends React.Component { this.state = { user: "", pwd: "", + captchaInput: "", }; this.initIsAuthed(); @@ -87,6 +104,10 @@ export class AuthPane extends React.Component { this.setState({ pwd: ev.target.value }); }; + changeCaptcha = (ev: React.ChangeEvent) => { + this.setState({ captchaInput: ev.target.value }); + }; + initIsAuthed = () => { Updater.initIsAuthed().then(() => { this.update(Updater.setAuthPane); @@ -94,7 +115,12 @@ export class AuthPane extends React.Component { }; login = () => { - Updater.login(this.state.user, this.state.pwd) + Updater.login( + this.state.user, + this.state.pwd, + this.props.captchaID, + this.state.captchaInput + ) .then((ok: boolean) => { if (ok) { this.update(Updater.setAuthPane); @@ -129,33 +155,6 @@ export class AuthPane extends React.Component { }; render() { - const elements: Array = [ - , - , - , - ]; - return (
{ style={{ display: this.props.authed ? "none" : "block" }} >
- {/*
Login
*/} - +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+
diff --git a/src/client/web/src/components/pane_settings.tsx b/src/client/web/src/components/pane_settings.tsx index 4938e71..2d8c2e1 100644 --- a/src/client/web/src/components/pane_settings.tsx +++ b/src/client/web/src/components/pane_settings.tsx @@ -3,7 +3,6 @@ import * as React from "react"; import { ICoreState } from "./core_state"; import { IUsersClient } from "../client"; import { AuthPane, Props as LoginProps } from "./pane_login"; -import { Layouter } from "./layouter"; import { UsersClient } from "../client/users"; export interface Props { @@ -174,6 +173,7 @@ export class PaneSettings extends React.Component {
diff --git a/src/client/web/src/components/panes.tsx b/src/client/web/src/components/panes.tsx index b18e667..07f378b 100644 --- a/src/client/web/src/components/panes.tsx +++ b/src/client/web/src/components/panes.tsx @@ -47,8 +47,7 @@ export class Updater { return true; } return false; - } - + }; static addUser = async (user: User): Promise => { const resp = await Updater.client.addUser(user.name, user.pwd, user.role); @@ -156,7 +155,11 @@ export class Panes extends React.Component { ), login: ( - + ), }); diff --git a/src/client/web/src/components/root_frame.tsx b/src/client/web/src/components/root_frame.tsx index 7415d1b..e15cc95 100644 --- a/src/client/web/src/components/root_frame.tsx +++ b/src/client/web/src/components/root_frame.tsx @@ -3,7 +3,6 @@ import * as React from "react"; import { ICoreState, BaseUpdater } from "./core_state"; import { Browser, Props as BrowserProps } from "./browser"; import { Props as PaneLoginProps } from "./pane_login"; -import { Props as PaneAdminProps } from "./pane_admin"; import { Panes, Props as PanesProps, Updater as PanesUpdater } from "./panes"; export interface Props { @@ -100,7 +99,6 @@ export class RootFrame extends React.Component { Quickshare - sharing in simple way. - ); diff --git a/src/client/web/src/components/state_mgr.tsx b/src/client/web/src/components/state_mgr.tsx index 55eaea5..6fb1aa2 100644 --- a/src/client/web/src/components/state_mgr.tsx +++ b/src/client/web/src/components/state_mgr.tsx @@ -6,6 +6,7 @@ import { ICoreState, init } from "./core_state"; import { RootFrame } from "./root_frame"; import { FilesClient } from "../client/files"; import { UsersClient } from "../client/users"; +import { Updater as LoginPaneUpdater } from "./pane_login"; export interface Props {} export interface State extends ICoreState {} @@ -20,6 +21,19 @@ export class StateMgr extends React.Component { initUpdaters = (state: ICoreState) => { BrowserUpdater().init(state.panel.browser); BrowserUpdater().setClients(new UsersClient(""), new FilesClient("")); + + LoginPaneUpdater.init(state.panel.authPane); + LoginPaneUpdater.setClient(new UsersClient("")); + LoginPaneUpdater.getCaptchaID() + .then((ok: boolean) => { + if (!ok) { + alert("failed to get captcha id"); + } else { + this.update(LoginPaneUpdater.setAuthPane); + console.log(LoginPaneUpdater) + } + }); + BrowserUpdater() .setHomeItems() .then(() => { diff --git a/src/handlers/multiusers/captcha.go b/src/handlers/multiusers/captcha.go new file mode 100644 index 0000000..ddd3201 --- /dev/null +++ b/src/handlers/multiusers/captcha.go @@ -0,0 +1,42 @@ +package multiusers + +import ( + "bytes" + "errors" + + "github.com/dchest/captcha" + "github.com/gin-gonic/gin" + + q "github.com/ihexxa/quickshare/src/handlers" +) + +type GetCaptchaIDResp struct { + CaptchaID string `json:"id"` +} + +func (h *MultiUsersSvc) GetCaptchaID(c *gin.Context) { + captchaID := captcha.New() + c.JSON(200, &GetCaptchaIDResp{CaptchaID: captchaID}) +} + +// path: /captchas/imgs?id=xxx +func (h *MultiUsersSvc) GetCaptchaImg(c *gin.Context) { + captchaID := c.Query(q.CaptchaIDParam) + if captchaID == "" { + c.JSON(q.ErrResp(c, 400, errors.New("empty captcha ID"))) + return + } + + capWidth := h.cfg.IntOr("Users.CaptchaWidth", 256) + capHeight := h.cfg.IntOr("Users.CaptchaHeight", 64) + + // TODO: improve performance + buf := new(bytes.Buffer) + err := captcha.WriteImage(buf, captchaID, capWidth, capHeight) + if err != nil { + c.JSON(q.ErrResp(c, 500, err)) + return + } + + c.Data(200, "image/png", buf.Bytes()) +} diff --git a/src/handlers/multiusers/handlers.go b/src/handlers/multiusers/handlers.go index 18436f6..9d63ba2 100644 --- a/src/handlers/multiusers/handlers.go +++ b/src/handlers/multiusers/handlers.go @@ -7,6 +7,7 @@ import ( "strconv" "time" + "github.com/dchest/captcha" "github.com/gin-gonic/gin" "github.com/ihexxa/gocfg" "golang.org/x/crypto/bcrypt" @@ -61,6 +62,8 @@ func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error) apiRuleCname(userstore.AdminRole, "DELETE", "/v1/fs/uploadings"): true, apiRuleCname(userstore.AdminRole, "GET", "/v1/fs/metadata"): true, apiRuleCname(userstore.AdminRole, "OPTIONS", "/v1/settings/health"): true, + apiRuleCname(userstore.AdminRole, "GET", "/v1/captchas/"): true, + apiRuleCname(userstore.AdminRole, "GET", "/v1/captchas/imgs"): true, // user rules apiRuleCname(userstore.UserRole, "GET", "/"): true, apiRuleCname(userstore.UserRole, "GET", publicPath): true, @@ -82,6 +85,8 @@ func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error) apiRuleCname(userstore.UserRole, "DELETE", "/v1/fs/uploadings"): true, apiRuleCname(userstore.UserRole, "GET", "/v1/fs/metadata"): true, apiRuleCname(userstore.UserRole, "OPTIONS", "/v1/settings/health"): true, + apiRuleCname(userstore.UserRole, "GET", "/v1/captchas/"): true, + apiRuleCname(userstore.UserRole, "GET", "/v1/captchas/imgs"): true, // visitor rules apiRuleCname(userstore.VisitorRole, "GET", "/"): true, apiRuleCname(userstore.VisitorRole, "GET", publicPath): true, @@ -90,6 +95,8 @@ func NewMultiUsersSvc(cfg gocfg.ICfg, deps *depidx.Deps) (*MultiUsersSvc, error) apiRuleCname(userstore.VisitorRole, "GET", "/v1/users/self"): true, apiRuleCname(userstore.VisitorRole, "GET", "/v1/fs/files"): true, apiRuleCname(userstore.VisitorRole, "OPTIONS", "/v1/settings/health"): true, + apiRuleCname(userstore.VisitorRole, "GET", "/v1/captchas/"): true, + apiRuleCname(userstore.VisitorRole, "GET", "/v1/captchas/imgs"): true, } return &MultiUsersSvc{ @@ -122,8 +129,10 @@ func (h *MultiUsersSvc) IsInited() bool { } type LoginReq struct { - User string `json:"user"` - Pwd string `json:"pwd"` + User string `json:"user"` + Pwd string `json:"pwd"` + CaptchaID string `json:"captchaId"` + CaptchaInput string `json:"captchaInput"` } func (h *MultiUsersSvc) Login(c *gin.Context) { @@ -133,6 +142,15 @@ func (h *MultiUsersSvc) Login(c *gin.Context) { return } + // TODO: add rate limiter for verifying + captchaEnabled := h.cfg.BoolOr("Users.CaptchaEnabled", true) + if captchaEnabled { + if !captcha.VerifyString(req.CaptchaID, req.CaptchaInput) { + c.JSON(q.ErrResp(c, 403, errors.New("login failed"))) + return + } + } + user, err := h.deps.Users().GetUserByName(req.User) if err != nil { c.JSON(q.ErrResp(c, 500, err)) diff --git a/src/handlers/util.go b/src/handlers/util.go index 726b1d3..d2e1c55 100644 --- a/src/handlers/util.go +++ b/src/handlers/util.go @@ -15,14 +15,15 @@ var ( FsDir = "files" FsRootDir = "files" - UserIDParam = "uid" - UserParam = "user" - PwdParam = "pwd" - NewPwdParam = "newpwd" - RoleParam = "role" - ExpireParam = "expire" - TokenCookie = "tk" - LastID = "lid" + UserIDParam = "uid" + UserParam = "user" + PwdParam = "pwd" + NewPwdParam = "newpwd" + RoleParam = "role" + ExpireParam = "expire" + CaptchaIDParam = "capid" + TokenCookie = "tk" + LastID = "lid" ErrAccessDenied = errors.New("access denied") ErrUnauthorized = errors.New("unauthorized") diff --git a/src/server/config.go b/src/server/config.go index 8bc7ced..7383260 100644 --- a/src/server/config.go +++ b/src/server/config.go @@ -17,6 +17,9 @@ type UsersCfg struct { CookieHttpOnly bool `json:"cookieHttpOnly" yaml:"cookieHttpOnly"` MinUserNameLen int `json:"minUserNameLen" yaml:"minUserNameLen"` MinPwdLen int `json:"minPwdLen" yaml:"minPwdLen"` + CaptchaWidth int `json:"captchaWidth" yaml:"captchaWidth"` + CaptchaHeight int `json:"captchaHeight" yaml:"captchaHeight"` + CaptchaEnabled bool `json:"captchaEnabled" yaml:"captchaEnabled"` } type Secrets struct { @@ -60,6 +63,9 @@ func DefaultConfig() (string, error) { CookieHttpOnly: true, MinUserNameLen: 4, MinPwdLen: 6, + CaptchaWidth: 256, + CaptchaHeight: 60, + CaptchaEnabled: true, }, Secrets: &Secrets{ TokenSecret: "", diff --git a/src/server/server.go b/src/server/server.go index 58b2593..37975df 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -189,6 +189,10 @@ func initHandlers(router *gin.Engine, cfg gocfg.ICfg, deps *depidx.Deps) (*gin.E rolesAPI.DELETE("/", userHdrs.DelRole) rolesAPI.GET("/list", userHdrs.ListRoles) + captchaAPI := v1.Group("/captchas") + captchaAPI.GET("/", userHdrs.GetCaptchaID) + captchaAPI.GET("/imgs", userHdrs.GetCaptchaImg) + filesAPI := v1.Group("/fs") filesAPI.POST("/files", fileHdrs.Create) filesAPI.DELETE("/files", fileHdrs.Delete) diff --git a/src/server/server_concurrency_test.go b/src/server/server_concurrency_test.go index c256b01..f122e6a 100644 --- a/src/server/server_concurrency_test.go +++ b/src/server/server_concurrency_test.go @@ -18,7 +18,8 @@ func TestConcurrency(t *testing.T) { "users": { "enableAuth": true, "minUserNameLen": 2, - "minPwdLen": 4 + "minPwdLen": 4, + "captchaEnabled": false }, "server": { "debug": true diff --git a/src/server/server_files_test.go b/src/server/server_files_test.go index 8c3debf..c3f23dc 100644 --- a/src/server/server_files_test.go +++ b/src/server/server_files_test.go @@ -21,7 +21,8 @@ func TestFileHandlers(t *testing.T) { "users": { "enableAuth": true, "minUserNameLen": 2, - "minPwdLen": 4 + "minPwdLen": 4, + "captchaEnabled": false }, "server": { "debug": true diff --git a/src/server/server_users_test.go b/src/server/server_users_test.go index bb41cdf..094f8da 100644 --- a/src/server/server_users_test.go +++ b/src/server/server_users_test.go @@ -19,7 +19,8 @@ func TestUsersHandlers(t *testing.T) { "users": { "enableAuth": true, "minUserNameLen": 2, - "minPwdLen": 4 + "minPwdLen": 4, + "captchaEnabled": false }, "server": { "debug": true