Refactorization and add metrics system.
This commit is contained in:
BIN
keystore.p12
Executable file → Normal file
BIN
keystore.p12
Executable file → Normal file
Binary file not shown.
2
pom.xml
2
pom.xml
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<groupId>org.codiki</groupId>
|
<groupId>org.codiki</groupId>
|
||||||
<artifactId>codiki</artifactId>
|
<artifactId>codiki</artifactId>
|
||||||
<version>1.0.1</version>
|
<version>1.1.0</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>codiki</name>
|
<name>codiki</name>
|
||||||
|
|||||||
@@ -46,8 +46,16 @@ public class AccountController {
|
|||||||
|
|
||||||
@JsonView(View.UserDTO.class)
|
@JsonView(View.UserDTO.class)
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public User login(@RequestBody final User pUser) throws BadCredentialsException {
|
public User login(@RequestBody final User pUser, HttpServletResponse pResponse) throws BadCredentialsException {
|
||||||
return accountService.authenticate(pUser);
|
User result = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = accountService.authenticate(pUser);
|
||||||
|
} catch(BadCredentialsException ex) {
|
||||||
|
pResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/logout")
|
@GetMapping("/logout")
|
||||||
|
|||||||
14
src/main/java/org/codiki/core/config/CoreConfig.java
Normal file
14
src/main/java/org/codiki/core/config/CoreConfig.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package org.codiki.core.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class CoreConfig {
|
||||||
|
@Bean
|
||||||
|
public Clock clock() {
|
||||||
|
return Clock.systemDefaultZone();
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/main/java/org/codiki/core/entities/dto/Metrics.java
Normal file
37
src/main/java/org/codiki/core/entities/dto/Metrics.java
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package org.codiki.core.entities.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class Metrics {
|
||||||
|
/** Rest API version number. */
|
||||||
|
private String version;
|
||||||
|
/** Application start date. */
|
||||||
|
private LocalDateTime uptime;
|
||||||
|
/** Platform name. */
|
||||||
|
private String platform;
|
||||||
|
|
||||||
|
public String getVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVersion(String version) {
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getUptime() {
|
||||||
|
return uptime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUptime(LocalDateTime uptime) {
|
||||||
|
this.uptime = uptime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPlatform() {
|
||||||
|
return platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlatform(String platform) {
|
||||||
|
this.platform = platform;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/main/java/org/codiki/metrics/MetricsController.java
Normal file
26
src/main/java/org/codiki/metrics/MetricsController.java
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package org.codiki.metrics;
|
||||||
|
|
||||||
|
import org.codiki.core.entities.dto.Metrics;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/metrics")
|
||||||
|
public class MetricsController {
|
||||||
|
|
||||||
|
private MetricsService metricsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param metricsService Metrics service.
|
||||||
|
*/
|
||||||
|
public MetricsController(MetricsService metricsService) {
|
||||||
|
this.metricsService = metricsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/healthCheck")
|
||||||
|
public Metrics healthCheck() {
|
||||||
|
return metricsService.getMetrics();
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/main/java/org/codiki/metrics/MetricsService.java
Normal file
44
src/main/java/org/codiki/metrics/MetricsService.java
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package org.codiki.metrics;
|
||||||
|
|
||||||
|
import org.codiki.core.entities.dto.Metrics;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MetricsService {
|
||||||
|
/** Application start date. */
|
||||||
|
private LocalDateTime uptime;
|
||||||
|
/** Application version number. */
|
||||||
|
private String appVersion;
|
||||||
|
/** Platform name. */
|
||||||
|
private String appPlatform;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param clock System clock.
|
||||||
|
*/
|
||||||
|
public MetricsService(Clock clock,
|
||||||
|
@Value("${app.version}") String appVersion,
|
||||||
|
@Value("${app.platform}") String appPlatform) {
|
||||||
|
uptime = LocalDateTime.now(clock);
|
||||||
|
this.appVersion = appVersion;
|
||||||
|
this.appPlatform = appPlatform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns application metrics like uptime, version number or platform name and others.
|
||||||
|
* @return Application metrics.
|
||||||
|
*/
|
||||||
|
public Metrics getMetrics() {
|
||||||
|
Metrics metrics = new Metrics();
|
||||||
|
|
||||||
|
metrics.setUptime(uptime);
|
||||||
|
metrics.setVersion(appVersion);
|
||||||
|
metrics.setPlatform(appPlatform);
|
||||||
|
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import org.codiki.core.entities.persistence.User;
|
|||||||
import org.codiki.core.repositories.PostRepository;
|
import org.codiki.core.repositories.PostRepository;
|
||||||
import org.codiki.core.services.ParserService;
|
import org.codiki.core.services.ParserService;
|
||||||
import org.codiki.core.services.UserService;
|
import org.codiki.core.services.UserService;
|
||||||
|
import org.codiki.core.utils.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
@@ -36,23 +37,37 @@ import com.fasterxml.jackson.annotation.JsonView;
|
|||||||
public class PostController {
|
public class PostController {
|
||||||
|
|
||||||
private static final int LIMIT_POSTS_HOME = 20;
|
private static final int LIMIT_POSTS_HOME = 20;
|
||||||
|
/** Service to parse post content. */
|
||||||
@Autowired
|
|
||||||
private ParserService parserService;
|
private ParserService parserService;
|
||||||
|
/** Posts repository. */
|
||||||
@Autowired
|
|
||||||
private PostRepository postRepository;
|
private PostRepository postRepository;
|
||||||
|
/** Posts business service. */
|
||||||
@Autowired
|
|
||||||
private PostService postService;
|
private PostService postService;
|
||||||
|
/** Users business service. */
|
||||||
@Autowired
|
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
* @param parserService Service to parse post content.
|
||||||
|
* @param postRepository Posts repository.
|
||||||
|
* @param postService Posts business service.
|
||||||
|
* @param userService Users business service.
|
||||||
|
*/
|
||||||
|
public PostController(ParserService parserService,
|
||||||
|
PostRepository postRepository,
|
||||||
|
PostService postService,
|
||||||
|
UserService userService) {
|
||||||
|
this.parserService = parserService;
|
||||||
|
this.postRepository = postRepository;
|
||||||
|
this.postService = postService;
|
||||||
|
this.userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public List<PostDTO> getAll() {
|
public List<PostDTO> getAll() {
|
||||||
return StreamSupport.stream(postRepository.findAll().spliterator(), false)
|
return StreamSupport.stream(postRepository.findAll().spliterator(), false)
|
||||||
.map(PostDTO::new).collect(Collectors.toList());
|
.map(PostDTO::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonView(View.PostDTO.class)
|
@JsonView(View.PostDTO.class)
|
||||||
@@ -70,16 +85,11 @@ public class PostController {
|
|||||||
@GetMapping("/{postKey}/source")
|
@GetMapping("/{postKey}/source")
|
||||||
public Post getByKeyAndSource(@PathVariable("postKey")final String pPostKey,
|
public Post getByKeyAndSource(@PathVariable("postKey")final String pPostKey,
|
||||||
final HttpServletResponse response) {
|
final HttpServletResponse response) {
|
||||||
Post result = null;
|
return postRepository.getByKey(pPostKey)
|
||||||
|
.orElseGet(() -> {
|
||||||
final Optional<Post> post = postRepository.getByKey(pPostKey);
|
|
||||||
if(post.isPresent()) {
|
|
||||||
result = post.get();
|
|
||||||
} else {
|
|
||||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
}
|
return null;
|
||||||
|
});
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonView(View.PostDTO.class)
|
@JsonView(View.PostDTO.class)
|
||||||
@@ -121,9 +131,10 @@ public class PostController {
|
|||||||
@JsonView(View.PostDTO.class)
|
@JsonView(View.PostDTO.class)
|
||||||
@PostMapping("/preview")
|
@PostMapping("/preview")
|
||||||
public Post preview(@RequestBody final Post pPost) {
|
public Post preview(@RequestBody final Post pPost) {
|
||||||
pPost.setImage(pPost.getImage() == null || "".equals(pPost.getImage())
|
pPost.setImage(StringUtils.isNull(pPost.getImage())
|
||||||
? "https://news-cdn.softpedia.com/images/news2/this-is-the-default-wallpaper-of-the-gnome-3-20-desktop-environment-500743-2.jpg"
|
? "https://news-cdn.softpedia.com/images/news2/this-is-the-default-wallpaper-of-the-gnome-3-20-desktop-environment-500743-2.jpg"
|
||||||
: pPost.getImage());
|
: pPost.getImage());
|
||||||
|
|
||||||
pPost.setText(parserService.parse(pPost.getText()));
|
pPost.setText(parserService.parse(pPost.getText()));
|
||||||
|
|
||||||
return pPost;
|
return pPost;
|
||||||
@@ -133,15 +144,7 @@ public class PostController {
|
|||||||
@PostMapping("/")
|
@PostMapping("/")
|
||||||
public Post insert(@RequestBody final PostDTO pPost, final HttpServletRequest pRequest,
|
public Post insert(@RequestBody final PostDTO pPost, final HttpServletRequest pRequest,
|
||||||
final HttpServletResponse pResponse, final Principal pPrincipal) {
|
final HttpServletResponse pResponse, final Principal pPrincipal) {
|
||||||
Post result = null;
|
return postService.insert(pPost, pRequest, pResponse, pPrincipal).orElse(null);
|
||||||
|
|
||||||
Optional<Post> postCreated = postService.insert(pPost, pRequest, pResponse, pPrincipal);
|
|
||||||
|
|
||||||
if(postCreated.isPresent()) {
|
|
||||||
result = postCreated.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/")
|
@PutMapping("/")
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
app:
|
app:
|
||||||
name: Codiki
|
name: Codiki
|
||||||
description: A wiki application.
|
description: A wiki application.
|
||||||
|
version: 1.2.0
|
||||||
|
platform: develop
|
||||||
|
|
||||||
codiki:
|
codiki:
|
||||||
files:
|
files:
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { ForbiddenComponent } from './forbidden/forbidden.component';
|
|||||||
import { SearchComponent } from './search/search.component';
|
import { SearchComponent } from './search/search.component';
|
||||||
import { SigninComponent } from './signin/signin.component';
|
import { SigninComponent } from './signin/signin.component';
|
||||||
import { VersionRevisionComponent } from './version-revisions/version-revisions.component';
|
import { VersionRevisionComponent } from './version-revisions/version-revisions.component';
|
||||||
|
import { HealthCheckComponent } from './health-check/health-check.component';
|
||||||
|
|
||||||
// Reusable components
|
// Reusable components
|
||||||
import { PostCardComponent } from './core/post-card/post-card.component';
|
import { PostCardComponent } from './core/post-card/post-card.component';
|
||||||
@@ -57,6 +58,7 @@ import { CreateUpdatePostService } from './posts/create-update/create-update-pos
|
|||||||
import { SearchService } from './search/search.service';
|
import { SearchService } from './search/search.service';
|
||||||
import { SigninService } from './signin/signin.service';
|
import { SigninService } from './signin/signin.service';
|
||||||
import { VersionRevisionService } from './version-revisions/version-revisions.service';
|
import { VersionRevisionService } from './version-revisions/version-revisions.service';
|
||||||
|
import { HealthCheckService } from './health-check/health-check.service';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@@ -81,7 +83,8 @@ import { VersionRevisionService } from './version-revisions/version-revisions.se
|
|||||||
SearchComponent,
|
SearchComponent,
|
||||||
SearchBarComponent,
|
SearchBarComponent,
|
||||||
ProgressBarComponent,
|
ProgressBarComponent,
|
||||||
ForbiddenComponent
|
ForbiddenComponent,
|
||||||
|
HealthCheckComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@@ -110,6 +113,7 @@ import { VersionRevisionService } from './version-revisions/version-revisions.se
|
|||||||
CreateUpdatePostService,
|
CreateUpdatePostService,
|
||||||
VersionRevisionService,
|
VersionRevisionService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
HealthCheckService,
|
||||||
{ provide: HTTP_INTERCEPTORS, useClass: UnauthorizedInterceptor, multi: true }
|
{ provide: HTTP_INTERCEPTORS, useClass: UnauthorizedInterceptor, multi: true }
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { PostComponent } from './posts/post.component';
|
|||||||
import { ByCategoryComponent } from './posts/byCategory/by-category.component';
|
import { ByCategoryComponent } from './posts/byCategory/by-category.component';
|
||||||
import { CreateUpdatePostComponent } from './posts/create-update/create-update-post.component';
|
import { CreateUpdatePostComponent } from './posts/create-update/create-update-post.component';
|
||||||
import { VersionRevisionComponent } from './version-revisions/version-revisions.component';
|
import { VersionRevisionComponent } from './version-revisions/version-revisions.component';
|
||||||
|
import { HealthCheckComponent } from './health-check/health-check.component';
|
||||||
import { SearchComponent } from './search/search.component';
|
import { SearchComponent } from './search/search.component';
|
||||||
|
|
||||||
export const appRoutes: Routes = [
|
export const appRoutes: Routes = [
|
||||||
@@ -31,6 +32,7 @@ export const appRoutes: Routes = [
|
|||||||
{ path: 'changePassword', component: ChangePasswordComponent, canActivate: [AuthGuard] },
|
{ path: 'changePassword', component: ChangePasswordComponent, canActivate: [AuthGuard] },
|
||||||
{ path: 'profilEdit', component: ProfilEditionComponent, canActivate: [AuthGuard] },
|
{ path: 'profilEdit', component: ProfilEditionComponent, canActivate: [AuthGuard] },
|
||||||
{ path: 'versionrevisions', component: VersionRevisionComponent },
|
{ path: 'versionrevisions', component: VersionRevisionComponent },
|
||||||
|
{ path: 'healthCheck', component: HealthCheckComponent },
|
||||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||||
{ path: '**', redirectTo: '/home' }
|
{ path: '**', redirectTo: '/home' }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -73,3 +73,11 @@ export class VersionRevision {
|
|||||||
public bugfix: boolean
|
public bugfix: boolean
|
||||||
) { }
|
) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class Metrics {
|
||||||
|
constructor(
|
||||||
|
public version: String,
|
||||||
|
public uptime: Date,
|
||||||
|
public platform: String
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
<i id="appVersion" routerLink="/versionrevisions" mdbTooltip="Notes de versions" placement="top" mdbRippleRadius>
|
<i id="appVersion" routerLink="/versionrevisions" mdbTooltip="Notes de versions" placement="top" mdbRippleRadius>
|
||||||
{{appVersion}}
|
{{appVersion}}
|
||||||
</i>
|
</i>
|
||||||
|
<i id="healthCheck"
|
||||||
|
class="fa fa-heartbeat"
|
||||||
|
routerLink="/healthCheck"
|
||||||
|
mdbTooltip="Ligne de vie"
|
||||||
|
placement="top"
|
||||||
|
mdbRippleRadius>
|
||||||
|
</i>
|
||||||
</span>
|
</span>
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
<a target="_blank" href="./assets/doc/codiki_user_manual.pdf" mdbTooltip="Manuel d'utilisation" placement="top" mdbRippleRadius>
|
<a target="_blank" href="./assets/doc/codiki_user_manual.pdf" mdbTooltip="Manuel d'utilisation" placement="top" mdbRippleRadius>
|
||||||
|
|||||||
@@ -23,3 +23,8 @@ span.anticopy {
|
|||||||
display: inline;
|
display: inline;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#healthCheck {
|
||||||
|
margin-left: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|||||||
23
src/main/ts/src/app/health-check/health-check.component.html
Normal file
23
src/main/ts/src/app/health-check/health-check.component.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<div class="col-md-8 offset-md-2 col-lg-6 offset-lg-3">
|
||||||
|
<mdb-card>
|
||||||
|
<!--Card content-->
|
||||||
|
<mdb-card-body>
|
||||||
|
<!--Title-->
|
||||||
|
<mdb-card-title>
|
||||||
|
<h4>Status du serveur</h4>
|
||||||
|
</mdb-card-title>
|
||||||
|
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item">
|
||||||
|
Plateforme <mdb-badge pill="true" primary="true" class="float-right">{{metrics.platform}}</mdb-badge>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
Version <mdb-badge pill="true" primary="true" class="float-right">{{metrics.version}}</mdb-badge>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
Démarré depuis {{metrics.uptime}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</mdb-card-body>
|
||||||
|
</mdb-card>
|
||||||
|
</div>
|
||||||
23
src/main/ts/src/app/health-check/health-check.component.ts
Normal file
23
src/main/ts/src/app/health-check/health-check.component.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { HealthCheckService } from './health-check.service';
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Metrics } from '../core/entities';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-health-check',
|
||||||
|
templateUrl: './health-check.component.html',
|
||||||
|
styleUrls: ['./health-check.component.scss']
|
||||||
|
})
|
||||||
|
export class HealthCheckComponent implements OnInit {
|
||||||
|
metrics: Metrics = new Metrics('1.0.0', null, '?');
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private healthCheckService: HealthCheckService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.healthCheckService.healthCheck().subscribe(metrics => {
|
||||||
|
this.metrics = metrics;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
16
src/main/ts/src/app/health-check/health-check.service.ts
Normal file
16
src/main/ts/src/app/health-check/health-check.service.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { Metrics } from '../core/entities';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class HealthCheckService {
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
healthCheck(): Observable<Metrics> {
|
||||||
|
return this.http.get<Metrics>('/api/metrics/healthCheck');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Post } from '../core/entities';
|
import { Post } from '../core/entities';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user