Lots of captcha fixes.
- Removed login captchas. - Added settings to disable captchas, and change difficulty. - Captchas can only be checked / used once, front end gives a new one on failure. - Added front end button for regenerating captcha. - Added a disabled / pause button audio playing.
This commit is contained in:
parent
1e1e87c9b2
commit
2becba4da7
10 changed files with 121 additions and 100 deletions
8
docs/src/contributing_websocket_http_api.md
vendored
8
docs/src/contributing_websocket_http_api.md
vendored
|
@ -360,8 +360,6 @@ The `jwt` string should be stored and used anywhere `auth` is called for.
|
|||
data: {
|
||||
username_or_email: String,
|
||||
password: String
|
||||
captcha_uuid: String,
|
||||
captcha_answer: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -393,8 +391,8 @@ Only the first user will be able to be the admin.
|
|||
password: String,
|
||||
password_verify: String,
|
||||
admin: bool,
|
||||
captcha_uuid: String,
|
||||
captcha_answer: String,
|
||||
captcha_uuid: Option<String>, // Only checked if these are enabled in the server
|
||||
captcha_answer: Option<String>,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -429,7 +427,7 @@ These expire after 10 minutes.
|
|||
data: {
|
||||
png: String, // A Base64 encoded png
|
||||
wav: Option<String>, // A Base64 encoded wav audio file
|
||||
uuid: String,
|
||||
uuid: String, // will return 'disabled' if server has these disabled
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
4
server/config/defaults.hjson
vendored
4
server/config/defaults.hjson
vendored
|
@ -59,6 +59,10 @@
|
|||
# comma seperated list of instances with which federation is allowed
|
||||
allowed_instances: ""
|
||||
}
|
||||
captcha: {
|
||||
enabled: true
|
||||
difficulty: medium # Can be easy, medium, or hard
|
||||
}
|
||||
# # email sending configuration
|
||||
# email: {
|
||||
# # hostname and port of the smtp server
|
||||
|
|
|
@ -17,6 +17,7 @@ pub struct Settings {
|
|||
pub rate_limit: RateLimitConfig,
|
||||
pub email: Option<EmailConfig>,
|
||||
pub federation: Federation,
|
||||
pub captcha: CaptchaConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
|
@ -46,6 +47,12 @@ pub struct EmailConfig {
|
|||
pub use_tls: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct CaptchaConfig {
|
||||
pub enabled: bool,
|
||||
pub difficulty: String, // easy, medium, or hard
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Database {
|
||||
pub user: String,
|
||||
|
|
|
@ -366,8 +366,8 @@ impl Perform for Oper<GetSite> {
|
|||
password_verify: setup.admin_password.to_owned(),
|
||||
admin: true,
|
||||
show_nsfw: true,
|
||||
captcha_uuid: "".to_string(),
|
||||
captcha_answer: "".to_string(),
|
||||
captcha_uuid: None,
|
||||
captcha_answer: None,
|
||||
};
|
||||
let login_response = Oper::new(register, self.client.clone())
|
||||
.perform(pool, websocket_info.clone())
|
||||
|
|
|
@ -59,8 +59,6 @@ use std::str::FromStr;
|
|||
pub struct Login {
|
||||
username_or_email: String,
|
||||
password: String,
|
||||
captcha_uuid: String,
|
||||
captcha_answer: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -71,8 +69,8 @@ pub struct Register {
|
|||
pub password_verify: String,
|
||||
pub admin: bool,
|
||||
pub show_nsfw: bool,
|
||||
pub captcha_uuid: String,
|
||||
pub captcha_answer: String,
|
||||
pub captcha_uuid: Option<String>,
|
||||
pub captcha_answer: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -82,7 +80,7 @@ pub struct GetCaptcha {}
|
|||
pub struct GetCaptchaResponse {
|
||||
png: String, // A Base64 encoded png
|
||||
wav: Option<String>, // A Base64 encoded wav audio
|
||||
uuid: String,
|
||||
uuid: String, // will be 'disabled' if captchas are disabled
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -285,7 +283,7 @@ impl Perform for Oper<Login> {
|
|||
async fn perform(
|
||||
&self,
|
||||
pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
_websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<LoginResponse, LemmyError> {
|
||||
let data: &Login = &self.data;
|
||||
|
||||
|
@ -306,27 +304,6 @@ impl Perform for Oper<Login> {
|
|||
return Err(APIError::err("password_incorrect").into());
|
||||
}
|
||||
|
||||
// Verify the captcha
|
||||
// Do an admin check, so that for your federation tests,
|
||||
// you don't need to solve the captchas
|
||||
if !user.admin {
|
||||
match websocket_info {
|
||||
Some(ws) => {
|
||||
let check = ws
|
||||
.chatserver
|
||||
.send(CheckCaptcha {
|
||||
uuid: data.captcha_uuid.to_owned(),
|
||||
answer: data.captcha_answer.to_owned(),
|
||||
})
|
||||
.await?;
|
||||
if !check {
|
||||
return Err(APIError::err("captcha_incorrect").into());
|
||||
}
|
||||
}
|
||||
None => return Err(APIError::err("captcha_incorrect").into()),
|
||||
};
|
||||
}
|
||||
|
||||
// Return the jwt
|
||||
Ok(LoginResponse {
|
||||
jwt: Claims::jwt(user, Settings::get().hostname),
|
||||
|
@ -359,14 +336,20 @@ impl Perform for Oper<Register> {
|
|||
}
|
||||
|
||||
// If its not the admin, check the captcha
|
||||
if !data.admin {
|
||||
if !data.admin && Settings::get().captcha.enabled {
|
||||
match websocket_info {
|
||||
Some(ws) => {
|
||||
let check = ws
|
||||
.chatserver
|
||||
.send(CheckCaptcha {
|
||||
uuid: data.captcha_uuid.to_owned(),
|
||||
answer: data.captcha_answer.to_owned(),
|
||||
uuid: data
|
||||
.captcha_uuid
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
answer: data
|
||||
.captcha_answer
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
})
|
||||
.await?;
|
||||
if !check {
|
||||
|
@ -505,7 +488,22 @@ impl Perform for Oper<GetCaptcha> {
|
|||
_pool: &DbPool,
|
||||
websocket_info: Option<WebsocketInfo>,
|
||||
) -> Result<Self::Response, LemmyError> {
|
||||
let captcha = gen(Difficulty::Medium);
|
||||
let captcha_settings = Settings::get().captcha;
|
||||
|
||||
if !captcha_settings.enabled {
|
||||
return Ok(GetCaptchaResponse {
|
||||
png: "disabled".to_string(),
|
||||
uuid: "disabled".to_string(),
|
||||
wav: None,
|
||||
});
|
||||
}
|
||||
|
||||
let captcha = match captcha_settings.difficulty.as_str() {
|
||||
"easy" => gen(Difficulty::Easy),
|
||||
"medium" => gen(Difficulty::Medium),
|
||||
"hard" => gen(Difficulty::Hard),
|
||||
_ => gen(Difficulty::Medium),
|
||||
};
|
||||
|
||||
let answer = captcha.chars_as_string();
|
||||
|
||||
|
|
|
@ -811,9 +811,14 @@ impl Handler<CheckCaptcha> for ChatServer {
|
|||
// Remove all the ones that are past the expire time
|
||||
self.captchas.retain(|x| x.expires.gt(&naive_now()));
|
||||
|
||||
self
|
||||
let check = self
|
||||
.captchas
|
||||
.iter()
|
||||
.any(|r| r.uuid == msg.uuid && r.answer == msg.answer)
|
||||
.any(|r| r.uuid == msg.uuid && r.answer == msg.answer);
|
||||
|
||||
// Remove this uuid so it can't be re-checked (Checks only work once)
|
||||
self.captchas.retain(|x| x.uuid != msg.uuid);
|
||||
|
||||
check
|
||||
}
|
||||
}
|
||||
|
|
6
ui/src/api_tests/api.spec.ts
vendored
6
ui/src/api_tests/api.spec.ts
vendored
|
@ -51,8 +51,6 @@ describe('main', () => {
|
|||
let form: LoginForm = {
|
||||
username_or_email: 'lemmy_alpha',
|
||||
password: 'lemmy',
|
||||
captcha_uuid: '', // Admins don't need this
|
||||
captcha_answer: '',
|
||||
};
|
||||
|
||||
let res: LoginResponse = await fetch(`${lemmyAlphaApiUrl}/user/login`, {
|
||||
|
@ -69,8 +67,6 @@ describe('main', () => {
|
|||
let formB = {
|
||||
username_or_email: 'lemmy_beta',
|
||||
password: 'lemmy',
|
||||
captcha_uuid: '', // Admins don't need this
|
||||
captcha_answer: '',
|
||||
};
|
||||
|
||||
let resB: LoginResponse = await fetch(`${lemmyBetaApiUrl}/user/login`, {
|
||||
|
@ -87,8 +83,6 @@ describe('main', () => {
|
|||
let formC = {
|
||||
username_or_email: 'lemmy_gamma',
|
||||
password: 'lemmy',
|
||||
captcha_uuid: '', // Admins don't need this
|
||||
captcha_answer: '',
|
||||
};
|
||||
|
||||
let resG: LoginResponse = await fetch(`${lemmyGammaApiUrl}/user/login`, {
|
||||
|
|
73
ui/src/components/login.tsx
vendored
73
ui/src/components/login.tsx
vendored
|
@ -22,6 +22,7 @@ interface State {
|
|||
registerLoading: boolean;
|
||||
enable_nsfw: boolean;
|
||||
captcha: GetCaptchaResponse;
|
||||
captchaPlaying: boolean;
|
||||
}
|
||||
|
||||
export class Login extends Component<any, State> {
|
||||
|
@ -31,8 +32,6 @@ export class Login extends Component<any, State> {
|
|||
loginForm: {
|
||||
username_or_email: undefined,
|
||||
password: undefined,
|
||||
captcha_uuid: undefined,
|
||||
captcha_answer: undefined,
|
||||
},
|
||||
registerForm: {
|
||||
username: undefined,
|
||||
|
@ -46,11 +45,8 @@ export class Login extends Component<any, State> {
|
|||
loginLoading: false,
|
||||
registerLoading: false,
|
||||
enable_nsfw: undefined,
|
||||
captcha: {
|
||||
png: undefined,
|
||||
uuid: undefined,
|
||||
wav: undefined,
|
||||
},
|
||||
captcha: undefined,
|
||||
captchaPlaying: false,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
|
@ -133,22 +129,6 @@ export class Login extends Component<any, State> {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2" htmlFor="login-captcha">
|
||||
{i18n.t('enter_code')}
|
||||
</label>
|
||||
{this.showCaptcha()}
|
||||
<div class="col-sm-6">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="login-captcha"
|
||||
value={this.state.loginForm.captcha_answer}
|
||||
onInput={linkEvent(this, this.handleLoginCaptchaAnswerChange)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
|
@ -253,9 +233,19 @@ export class Login extends Component<any, State> {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.captcha && (
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2" htmlFor="register-captcha">
|
||||
{i18n.t('enter_code')}
|
||||
<span class="mr-2">{i18n.t('enter_code')}</span>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
onClick={linkEvent(this, this.handleRegenCaptcha)}
|
||||
>
|
||||
<svg class="icon icon-refresh-cw">
|
||||
<use xlinkHref="#icon-refresh-cw"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</label>
|
||||
{this.showCaptcha()}
|
||||
<div class="col-sm-6">
|
||||
|
@ -264,11 +254,15 @@ export class Login extends Component<any, State> {
|
|||
class="form-control"
|
||||
id="register-captcha"
|
||||
value={this.state.registerForm.captcha_answer}
|
||||
onInput={linkEvent(this, this.handleRegisterCaptchaAnswerChange)}
|
||||
onInput={linkEvent(
|
||||
this,
|
||||
this.handleRegisterCaptchaAnswerChange
|
||||
)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{this.state.enable_nsfw && (
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
|
@ -320,10 +314,17 @@ export class Login extends Component<any, State> {
|
|||
style="border-top-right-radius: 0; border-top-left-radius: 0;"
|
||||
title={i18n.t('play_captcha_audio')}
|
||||
onClick={linkEvent(this, this.handleCaptchaPlay)}
|
||||
disabled={this.state.captchaPlaying}
|
||||
>
|
||||
{!this.state.captchaPlaying ? (
|
||||
<svg class="icon icon-play">
|
||||
<use xlinkHref="#icon-play"></use>
|
||||
</svg>
|
||||
) : (
|
||||
<svg class="icon icon-pause">
|
||||
<use xlinkHref="#icon-pause"></use>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
|
@ -384,16 +385,16 @@ export class Login extends Component<any, State> {
|
|||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleLoginCaptchaAnswerChange(i: Login, event: any) {
|
||||
i.state.loginForm.captcha_answer = event.target.value;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleRegisterCaptchaAnswerChange(i: Login, event: any) {
|
||||
i.state.registerForm.captcha_answer = event.target.value;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleRegenCaptcha(_i: Login, _event: any) {
|
||||
event.preventDefault();
|
||||
WebSocketService.Instance.getCaptcha();
|
||||
}
|
||||
|
||||
handlePasswordReset(i: Login) {
|
||||
event.preventDefault();
|
||||
let resetForm: PasswordResetForm = {
|
||||
|
@ -406,6 +407,13 @@ export class Login extends Component<any, State> {
|
|||
event.preventDefault();
|
||||
let snd = new Audio('data:audio/wav;base64,' + i.state.captcha.wav);
|
||||
snd.play();
|
||||
i.state.captchaPlaying = true;
|
||||
i.setState(i.state);
|
||||
snd.addEventListener('ended', () => {
|
||||
snd.currentTime = 0;
|
||||
i.state.captchaPlaying = false;
|
||||
i.setState(this.state);
|
||||
});
|
||||
}
|
||||
|
||||
captchaPngSrc() {
|
||||
|
@ -417,6 +425,8 @@ export class Login extends Component<any, State> {
|
|||
if (msg.error) {
|
||||
toast(i18n.t(msg.error), 'danger');
|
||||
this.state = this.emptyState;
|
||||
// Refetch another captcha
|
||||
WebSocketService.Instance.getCaptcha();
|
||||
this.setState(this.state);
|
||||
return;
|
||||
} else {
|
||||
|
@ -437,10 +447,11 @@ export class Login extends Component<any, State> {
|
|||
this.props.history.push('/communities');
|
||||
} else if (res.op == UserOperation.GetCaptcha) {
|
||||
let data = res.data as GetCaptchaResponse;
|
||||
if (data.uuid != 'disabled') {
|
||||
this.state.captcha = data;
|
||||
this.state.loginForm.captcha_uuid = data.uuid;
|
||||
this.state.registerForm.captcha_uuid = data.uuid;
|
||||
this.setState(this.state);
|
||||
}
|
||||
} else if (res.op == UserOperation.PasswordReset) {
|
||||
toast(i18n.t('reset_password_mail_sent'));
|
||||
} else if (res.op == UserOperation.GetSite) {
|
||||
|
|
6
ui/src/components/symbols.tsx
vendored
6
ui/src/components/symbols.tsx
vendored
|
@ -15,6 +15,12 @@ export class Symbols extends Component<any, any> {
|
|||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<defs>
|
||||
<symbol id="icon-refresh-cw" viewBox="0 0 24 24">
|
||||
<path d="M4.453 9.334c0.737-2.083 2.247-3.669 4.096-4.552s4.032-1.059 6.114-0.322c1.186 0.42 2.206 1.088 2.983 1.88l2.83 2.66h-3.476c-0.552 0-1 0.448-1 1s0.448 1 1 1h5.997c0.005 0 0.009 0 0.014 0 0.137-0.001 0.268-0.031 0.386-0.082 0.119-0.051 0.229-0.126 0.324-0.225 0.012-0.013 0.024-0.026 0.036-0.039 0.075-0.087 0.133-0.183 0.173-0.285s0.064-0.211 0.069-0.326c0.001-0.015 0.001-0.029 0.001-0.043v-6c0-0.552-0.448-1-1-1s-1 0.448-1 1v3.689l-2.926-2.749c-0.992-1.010-2.271-1.843-3.743-2.364-2.603-0.921-5.335-0.699-7.643 0.402s-4.199 3.086-5.12 5.689c-0.185 0.52 0.088 1.091 0.608 1.276s1.092-0.088 1.276-0.609zM2 16.312l2.955 2.777c1.929 1.931 4.49 2.908 7.048 2.909s5.119-0.975 7.072-2.927c1.104-1.104 1.901-2.407 2.361-3.745 0.18-0.522-0.098-1.091-0.621-1.271s-1.091 0.098-1.271 0.621c-0.361 1.050-0.993 2.091-1.883 2.981-1.563 1.562-3.609 2.342-5.657 2.342s-4.094-0.782-5.679-2.366l-2.8-2.633h3.475c0.552 0 1-0.448 1-1s-0.448-1-1-1h-5.997c-0.005 0-0.009 0-0.014 0-0.137 0.001-0.268 0.031-0.386 0.082-0.119 0.051-0.229 0.126-0.324 0.225-0.012 0.013-0.024 0.026-0.036 0.039-0.075 0.087-0.133 0.183-0.173 0.285s-0.064 0.211-0.069 0.326c-0.001 0.015-0.001 0.029-0.001 0.043v6c0 0.552 0.448 1 1 1s1-0.448 1-1z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-pause" viewBox="0 0 24 24">
|
||||
<path d="M6 3c-0.552 0-1 0.448-1 1v16c0 0.552 0.448 1 1 1h4c0.552 0 1-0.448 1-1v-16c0-0.552-0.448-1-1-1zM7 5h2v14h-2zM14 3c-0.552 0-1 0.448-1 1v16c0 0.552 0.448 1 1 1h4c0.552 0 1-0.448 1-1v-16c0-0.552-0.448-1-1-1zM15 5h2v14h-2z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-play" viewBox="0 0 24 24">
|
||||
<path d="M5.541 2.159c-0.153-0.1-0.34-0.159-0.541-0.159-0.552 0-1 0.448-1 1v18c-0.001 0.182 0.050 0.372 0.159 0.541 0.299 0.465 0.917 0.599 1.382 0.3l14-9c0.114-0.072 0.219-0.174 0.3-0.3 0.299-0.465 0.164-1.083-0.3-1.382zM6 4.832l11.151 7.168-11.151 7.168z"></path>
|
||||
</symbol>
|
||||
|
|
6
ui/src/interfaces.ts
vendored
6
ui/src/interfaces.ts
vendored
|
@ -549,8 +549,6 @@ export interface ModAdd {
|
|||
export interface LoginForm {
|
||||
username_or_email: string;
|
||||
password: string;
|
||||
captcha_uuid: string;
|
||||
captcha_answer: string;
|
||||
}
|
||||
|
||||
export interface RegisterForm {
|
||||
|
@ -560,8 +558,8 @@ export interface RegisterForm {
|
|||
password_verify: string;
|
||||
admin: boolean;
|
||||
show_nsfw: boolean;
|
||||
captcha_uuid: string;
|
||||
captcha_answer: string;
|
||||
captcha_uuid?: string;
|
||||
captcha_answer?: string;
|
||||
}
|
||||
|
||||
export interface GetCaptchaResponse {
|
||||
|
|
Loading…
Reference in a new issue