Skip to content
Projects
Groups
Snippets
Help
Sign in / Register
Toggle navigation
Minds Frontend
Project overview
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Locked Files
Issues
384
Merge Requests
61
CI / CD
Security & Compliance
Packages
Analytics
Wiki
Snippets
Members
Collapse sidebar
Close sidebar
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Minds
Minds Frontend
Commits
9c924ddb
Commit
9c924ddb
authored
2 hours ago
by
Mark Harding
Browse files
Options
Download
(feat): implements captcha -
#646
parent
759cb8aa
feat/646-captcha
1 merge request
!760
Implements captcha - #646
Pipeline
#114897099
running with stages
Changes
14
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
147 additions
and
298 deletions
+147
-298
src/app/common/common.module.ts
View file @
9c924ddb
...
...
@@ -47,7 +47,6 @@ import { ScrollLock } from './directives/scroll-lock';
import
{
TagsLinks
}
from
'
./directives/tags
'
;
import
{
Tooltip
}
from
'
./directives/tooltip
'
;
import
{
MindsAvatar
}
from
'
./components/avatar/avatar
'
;
import
{
CaptchaComponent
}
from
'
./components/captcha/captcha.component
'
;
import
{
Textarea
}
from
'
./components/editors/textarea.component
'
;
import
{
TagcloudComponent
}
from
'
./components/tagcloud/tagcloud.component
'
;
import
{
DropdownComponent
}
from
'
./components/dropdown/dropdown.component
'
;
...
...
@@ -209,7 +208,6 @@ const routes: Routes = [
MDL_DIRECTIVES
,
DateSelectorComponent
,
MindsAvatar
,
CaptchaComponent
,
Textarea
,
InlineEditorComponent
,
...
...
@@ -316,7 +314,6 @@ const routes: Routes = [
MDL_DIRECTIVES
,
DateSelectorComponent
,
MindsAvatar
,
CaptchaComponent
,
Textarea
,
InlineEditorComponent
,
...
...
This diff is collapsed.
src/app/common/components/captcha/captcha.component.html
deleted
100644 → 0
View file @
759cb8aa
<div
class=
"m-captcha--sum"
*ngIf=
"type == 'sum'"
>
<div
class=
"m-captcha--sum-question "
*ngIf=
"question"
i18n=
"A sum (eg. 2 + 2)@@COMMON__CAPTCHA__SIMPLE_SUM"
>
What is {{ question[0] }} {{ question[1] }} {{ question[2] }}?
</div>
<input
type=
"number"
[(ngModel)]=
"answer"
(keyup)=
"validate()"
/>
</div>
This diff is collapsed.
src/app/common/components/captcha/captcha.component.scss
deleted
100644 → 0
View file @
759cb8aa
.m-captcha--sum
{
text-align
:
left
;
.m-captcha--sum-question
{
font-size
:
18px
;
padding
:
8px
;
letter-spacing
:
1px
;
font-family
:
'Roboto'
,
Helvetica
,
sans-serif
;
font-weight
:
600
;
display
:
inline-block
;
}
input
[
type
=
'number'
]
{
display
:
inline-block
;
width
:
46px
;
font-size
:
22px
;
padding
:
8px
0px
;
text-align
:
center
;
font-weight
:
600
;
font-family
:
'Roboto'
,
Helvetica
,
sans-serif
;
box-sizing
:
content-box
;
}
}
This diff is collapsed.
src/app/common/components/captcha/captcha.component.ts
deleted
100644 → 0
View file @
759cb8aa
import
{
Component
,
Output
,
Input
,
EventEmitter
}
from
'
@angular/core
'
;
import
{
Client
}
from
'
../../../services/api
'
;
@
Component
({
selector
:
'
m-captcha
'
,
templateUrl
:
'
captcha.component.html
'
,
})
export
class
CaptchaComponent
{
answer
:
string
|
number
;
@
Output
(
'
answer
'
)
emit
:
EventEmitter
<
any
>
=
new
EventEmitter
();
inProgress
:
boolean
=
false
;
type
:
string
=
'
sum
'
;
question
:
Array
<
string
|
number
>
;
nonce
:
number
;
hash
:
string
=
''
;
interval
;
constructor
(
public
client
:
Client
)
{}
ngOnInit
()
{
this
.
get
();
this
.
interval
=
setInterval
(
this
.
get
,
1000
*
60
*
4
);
//refresh every 4 minutes
}
ngOnDestroy
()
{
clearInterval
(
this
.
interval
);
}
get
()
{
this
.
client
.
get
(
'
api/v1/captcha
'
).
then
((
response
:
any
)
=>
{
this
.
type
=
response
.
question
.
type
;
this
.
question
=
response
.
question
.
question
;
this
.
nonce
=
response
.
question
.
nonce
;
this
.
hash
=
response
.
question
.
hash
;
});
}
validate
()
{
let
payload
=
{
type
:
this
.
type
,
question
:
this
.
question
,
answer
:
this
.
answer
,
nonce
:
this
.
nonce
,
hash
:
this
.
hash
,
};
this
.
emit
.
next
(
JSON
.
stringify
(
payload
));
this
.
client
.
post
(
'
api/v1/captcha
'
,
payload
).
then
((
response
:
any
)
=>
{
if
(
response
.
success
)
console
.
log
(
'
success
'
);
else
console
.
log
(
'
error
'
);
});
}
}
This diff is collapsed.
src/app/common/components/captcha/captcha.service.ts
deleted
100644 → 0
View file @
759cb8aa
export
class
CaptchaService
{}
This diff is collapsed.
src/app/modules/captcha/captcha.component.html
0 → 100644
View file @
9c924ddb
<ng-container
*ngIf=
"captcha"
>
<img
[src]=
"captcha.base64Image"
/>
<i
class=
"material-icons m-captcha__refresh"
(click)=
"refresh()"
>
refresh
</i>
<input
[ngModel]=
"captcha.clientText"
(ngModelChange)=
"onValueChange($event)"
type=
"text"
placeholder=
"Enter the characters above"
/>
</ng-container>
This diff is collapsed.
src/app/modules/captcha/captcha.component.scss
0 → 100644
View file @
9c924ddb
m-captcha
{
display
:
block
;
img
{
margin-bottom
:
$minds-margin
;
}
}
.m-captcha__refresh
{
cursor
:
pointer
;
position
:
absolute
;
}
This diff is collapsed.
src/app/modules/captcha/captcha.component.spec.ts
0 → 100644
View file @
9c924ddb
import
{
async
,
ComponentFixture
,
fakeAsync
,
TestBed
,
tick
,
}
from
'
@angular/core/testing
'
;
import
{
DebugElement
}
from
'
@angular/core
'
;
import
{
CaptchaComponent
,
Captcha
}
from
'
./captcha.component
'
;
import
{
ReactiveFormsModule
}
from
'
@angular/forms
'
;
import
{
Client
}
from
'
../../services/api
'
;
import
{
clientMock
}
from
'
../../../tests/client-mock.spec
'
;
import
{
By
}
from
'
@angular/platform-browser
'
;
describe
(
'
CaptchaComponent
'
,
()
=>
{
let
comp
:
CaptchaComponent
;
let
fixture
:
ComponentFixture
<
CaptchaComponent
>
;
beforeEach
(
async
(()
=>
{
TestBed
.
configureTestingModule
({
declarations
:
[
CaptchaComponent
],
imports
:
[
ReactiveFormsModule
],
providers
:
[{
provide
:
Client
,
useValue
:
clientMock
}],
}).
compileComponents
();
}));
beforeEach
(()
=>
{
fixture
=
TestBed
.
createComponent
(
CaptchaComponent
);
comp
=
fixture
.
componentInstance
;
fixture
.
detectChanges
();
clientMock
.
response
=
{};
});
});
This diff is collapsed.
src/app/modules/captcha/captcha.component.ts
0 → 100644
View file @
9c924ddb
import
{
Component
,
ElementRef
,
forwardRef
,
OnChanges
,
OnInit
,
ViewChild
,
}
from
'
@angular/core
'
;
import
{
ControlValueAccessor
,
NG_VALUE_ACCESSOR
}
from
'
@angular/forms
'
;
import
{
Client
}
from
'
../../services/api
'
;
export
const
CAPTCHA_VALUE_ACCESSOR
:
any
=
{
provide
:
NG_VALUE_ACCESSOR
,
useExisting
:
forwardRef
(()
=>
CaptchaComponent
),
multi
:
true
,
};
export
class
Captcha
{
jwtToken
:
string
;
base64Image
:
string
;
clientText
:
string
;
// This is what the user enters
buildClientKey
():
string
{
return
JSON
.
stringify
({
jwtToken
:
this
.
jwtToken
,
clientText
:
this
.
clientText
,
});
}
}
@
Component
({
selector
:
'
m-captcha
'
,
templateUrl
:
'
captcha.component.html
'
,
providers
:
[
CAPTCHA_VALUE_ACCESSOR
],
})
export
class
CaptchaComponent
implements
ControlValueAccessor
,
OnInit
{
captcha
=
new
Captcha
();
image
:
string
;
value
:
string
=
''
;
propagateChange
=
(
_
:
any
)
=>
{};
constructor
(
private
client
:
Client
)
{}
ngOnInit
():
void
{
this
.
refresh
();
}
async
refresh
():
Promise
<
void
>
{
const
response
:
any
=
await
this
.
client
.
get
(
'
api/v2/captcha
'
,
{
cb
:
Date
.
now
(),
});
this
.
captcha
.
base64Image
=
response
.
base64_image
;
this
.
captcha
.
jwtToken
=
response
.
jwt_token
;
}
onValueChange
(
value
:
string
)
{
this
.
captcha
.
clientText
=
value
;
this
.
value
=
this
.
captcha
.
buildClientKey
();
this
.
propagateChange
(
this
.
value
);
}
writeValue
(
value
:
any
):
void
{
// Not required as captcha is one direction
}
registerOnChange
(
fn
:
any
):
void
{
this
.
propagateChange
=
fn
;
}
registerOnTouched
(
fn
:
any
):
void
{}
}
This diff is collapsed.
src/app/modules/captcha/captcha.module.ts
View file @
9c924ddb
import
{
NgModule
}
from
'
@angular/core
'
;
import
{
ReCaptchaComponent
}
from
'
./recaptcha/recaptcha.component
'
;
import
{
RECAPTCHA_SERVICE_PROVIDER
}
from
'
./recaptcha/recaptcha.service
'
;
import
{
CaptchaComponent
}
from
'
./captcha.component
'
;
import
{
FormsModule
}
from
'
@angular/forms
'
;
import
{
CommonModule
}
from
'
@angular/common
'
;
@
NgModule
({
declarations
:
[
ReCaptchaComponent
],
exports
:
[
Re
CaptchaComponent
],
providers
:
[
RECAPTCHA_SERVICE_PROVIDER
],
imports
:
[
CommonModule
,
FormsModule
],
declarations
:
[
CaptchaComponent
],
exports
:
[
CaptchaComponent
],
})
export
class
CaptchaModule
{}
This diff is collapsed.
src/app/modules/captcha/recaptcha/recaptcha.component.ts
deleted
100644 → 0
View file @
759cb8aa
import
{
Component
,
OnInit
,
Input
,
Output
,
EventEmitter
,
NgZone
,
ViewChild
,
ElementRef
,
forwardRef
,
}
from
'
@angular/core
'
;
import
{
NG_VALUE_ACCESSOR
,
ControlValueAccessor
}
from
'
@angular/forms
'
;
import
{
ReCaptchaService
}
from
'
./recaptcha.service
'
;
@
Component
({
selector
:
'
re-captcha
'
,
template
:
'
<div #target></div>
'
,
providers
:
[
{
provide
:
NG_VALUE_ACCESSOR
,
useExisting
:
forwardRef
(()
=>
ReCaptchaComponent
),
multi
:
true
,
},
],
})
export
class
ReCaptchaComponent
implements
OnInit
,
ControlValueAccessor
{
@
Input
()
site_key
:
string
=
null
;
@
Input
()
theme
=
'
light
'
;
@
Input
()
type
=
'
image
'
;
@
Input
()
size
=
'
normal
'
;
@
Input
()
tabindex
=
0
;
@
Input
()
badge
=
'
bottomright
'
;
/* Available languages: https://developers.google.com/recaptcha/docs/language */
@
Input
()
language
:
string
=
null
;
@
Output
()
captchaResponse
=
new
EventEmitter
<
string
>
();
@
Output
()
captchaExpired
=
new
EventEmitter
();
@
ViewChild
(
'
target
'
,
{
static
:
true
})
targetRef
:
ElementRef
;
widgetId
:
any
=
null
;
onChange
:
Function
=
()
=>
{
return
;
};
onTouched
:
Function
=
()
=>
{
return
;
};
constructor
(
private
_zone
:
NgZone
,
private
_captchaService
:
ReCaptchaService
)
{}
ngOnInit
()
{
this
.
_captchaService
.
getReady
(
this
.
language
).
subscribe
(
ready
=>
{
if
(
!
ready
)
return
;
// noinspection TypeScriptUnresolvedVariable,TypeScriptUnresolvedFunction
this
.
widgetId
=
(
<
any
>
window
).
grecaptcha
.
render
(
this
.
targetRef
.
nativeElement
,
{
sitekey
:
this
.
site_key
,
badge
:
this
.
badge
,
theme
:
this
.
theme
,
type
:
this
.
type
,
size
:
this
.
size
,
tabindex
:
this
.
tabindex
,
callback
:
<
any
>
(
((
response
:
any
)
=>
this
.
_zone
.
run
(
this
.
recaptchaCallback
.
bind
(
this
,
response
)))
),
'
expired-callback
'
:
<
any
>
(
(()
=>
this
.
_zone
.
run
(
this
.
recaptchaExpiredCallback
.
bind
(
this
)))
),
}
);
});
}
// noinspection JSUnusedGlobalSymbols
public
reset
()
{
if
(
this
.
widgetId
===
null
)
return
;
// noinspection TypeScriptUnresolvedVariable
(
<
any
>
window
).
grecaptcha
.
reset
(
this
.
widgetId
);
this
.
onChange
(
null
);
}
// noinspection JSUnusedGlobalSymbols
public
execute
()
{
if
(
this
.
widgetId
===
null
)
return
;
// noinspection TypeScriptUnresolvedVariable
(
<
any
>
window
).
grecaptcha
.
execute
(
this
.
widgetId
);
}
public
getResponse
():
string
{
if
(
this
.
widgetId
===
null
)
return
null
;
// noinspection TypeScriptUnresolvedVariable
return
(
<
any
>
window
).
grecaptcha
.
getResponse
(
this
.
widgetId
);
}
writeValue
(
newValue
:
any
):
void
{
/* ignore it */
}
registerOnChange
(
fn
:
any
):
void
{
this
.
onChange
=
fn
;
}
registerOnTouched
(
fn
:
any
):
void
{
this
.
onTouched
=
fn
;
}
private
recaptchaCallback
(
response
:
string
)
{
this
.
onChange
(
response
);
this
.
onTouched
();
this
.
captchaResponse
.
emit
(
response
);
}
private
recaptchaExpiredCallback
()
{
this
.
onChange
(
null
);
this
.
onTouched
();
this
.
captchaExpired
.
emit
();
}
}
This diff is collapsed.
src/app/modules/captcha/recaptcha/recaptcha.service.ts
deleted
100644 → 0
View file @
759cb8aa
import
{
Injectable
,
NgZone
,
Optional
,
SkipSelf
}
from
'
@angular/core
'
;
import
{
Observable
,
BehaviorSubject
}
from
'
rxjs
'
;
/*
* Common service shared by all reCaptcha component instances
* through dependency injection.
* This service has the task of loading the reCaptcha API once for all.
* Only the first instance of the component creates the service, subsequent
* components will use the existing instance.
*
* As the language is passed to the <script>, the first component
* determines the language of all subsequent components. This is a limitation
* of the present Google API.
*/
@
Injectable
()
export
class
ReCaptchaService
{
private
scriptLoaded
=
false
;
private
readySubject
:
BehaviorSubject
<
boolean
>
=
new
BehaviorSubject
(
false
);
constructor
(
zone
:
NgZone
)
{
/* the callback needs to exist before the API is loaded */
window
[
<
any
>
'
reCaptchaOnloadCallback
'
]
=
<
any
>
(
(()
=>
zone
.
run
(
this
.
onloadCallback
.
bind
(
this
)))
);
}
public
getReady
(
language
:
string
):
Observable
<
boolean
>
{
if
(
!
this
.
scriptLoaded
)
{
this
.
scriptLoaded
=
true
;
let
doc
=
<
HTMLDivElement
>
document
.
body
;
let
script
=
document
.
createElement
(
'
script
'
);
script
.
innerHTML
=
''
;
script
.
src
=
'
https://www.google.com/recaptcha/api.js?onload=reCaptchaOnloadCallback&render=explicit
'
+
(
language
?
'
&hl=
'
+
language
:
''
);
script
.
async
=
true
;
script
.
defer
=
true
;
doc
.
appendChild
(
script
);
}
return
this
.
readySubject
.
asObservable
();
}
private
onloadCallback
()
{
this
.
readySubject
.
next
(
true
);
}
}
/* singleton pattern taken from https://github.com/angular/angular/issues/13854 */
export
function
RECAPTCHA_SERVICE_PROVIDER_FACTORY
(
ngZone
:
NgZone
,
parentDispatcher
:
ReCaptchaService
)
{
return
parentDispatcher
||
new
ReCaptchaService
(
ngZone
);
}
export
const
RECAPTCHA_SERVICE_PROVIDER
=
{
provide
:
ReCaptchaService
,
deps
:
[
NgZone
,
[
new
Optional
(),
new
SkipSelf
(),
ReCaptchaService
]],
useFactory
:
RECAPTCHA_SERVICE_PROVIDER_FACTORY
,
};
This diff is collapsed.
src/app/modules/forms/register/register.html
View file @
9c924ddb
...
...
@@ -134,6 +134,16 @@
</ng-container>
</div>
</div>
<div
*ngIf=
"form.value.password"
class=
"mdl-cell mdl-cell--12-col m-registerForm__captcha"
>
<label
for=
"captcha"
*ngIf=
"showLabels"
i18n
>
Captcha
</label>
<m-captcha
formControlName=
"captcha"
></m-captcha>
</div>
</div>
<div
...
...
This diff is collapsed.
src/app/modules/forms/register/register.ts
View file @
9c924ddb
...
...
@@ -17,7 +17,6 @@ import {
import
{
Client
}
from
'
../../../services/api
'
;
import
{
Session
}
from
'
../../../services/session
'
;
import
{
ReCaptchaComponent
}
from
'
../../../modules/captcha/recaptcha/recaptcha.component
'
;
import
{
ExperimentsService
}
from
'
../../experiments/experiments.service
'
;
import
{
RouterHistoryService
}
from
'
../../../common/services/router-history.service
'
;
import
{
PopoverComponent
}
from
'
../popover-validation/popover.component
'
;
...
...
@@ -28,7 +27,7 @@ import { FeaturesService } from '../../../services/features.service';
selector
:
'
minds-form-register
'
,
templateUrl
:
'
register.html
'
,
})
export
class
RegisterForm
implements
OnInit
{
export
class
RegisterForm
{
@
Input
()
referrer
:
string
;
@
Input
()
parentId
:
string
=
''
;
@
Input
()
showTitle
:
boolean
=
false
;
...
...
@@ -53,7 +52,6 @@ export class RegisterForm implements OnInit {
form
:
FormGroup
;
fbForm
:
FormGroup
;
@
ViewChild
(
'
reCaptcha
'
,
{
static
:
false
})
reCaptcha
:
ReCaptchaComponent
;
@
ViewChild
(
'
popover
'
,
{
static
:
false
})
popover
:
PopoverComponent
;
constructor
(
...
...
@@ -86,12 +84,6 @@ export class RegisterForm implements OnInit {
);
}
ngOnInit
()
{
if
(
this
.
reCaptcha
)
{
this
.
reCaptcha
.
reset
();
}
}
showError
(
field
:
string
)
{
return
(
this
.
showInlineErrors
&&
...
...
@@ -121,10 +113,6 @@ export class RegisterForm implements OnInit {
'
disabled_cookies=; expires=Thu, 01 Jan 1970 00:00:01 GMT;
'
;
if
(
this
.
form
.
value
.
password
!==
this
.
form
.
value
.
password2
)
{
if
(
this
.
reCaptcha
)
{
this
.
reCaptcha
.
reset
();
}
this
.
errorMessage
=
'
Passwords must match.
'
;
return
;
}
...
...
@@ -148,9 +136,6 @@ export class RegisterForm implements OnInit {
.
catch
(
e
=>
{
console
.
log
(
e
);
this
.
inProgress
=
false
;
if
(
this
.
reCaptcha
)
{
this
.
reCaptcha
.
reset
();
}
if
(
e
.
status
===
'
failed
'
)
{
// incorrect login details
...
...
This diff is collapsed.
Please
register
or
sign in
to comment