Merge pull request 'api-service' (#38) from api-service into master

Reviewed-on: http://git.plannaplan.pl/filipizydorczyk/backend/pulls/38
This commit is contained in:
Marcin Woźniak 2020-12-31 14:33:50 +01:00
commit 3bce1f91ec
17 changed files with 423 additions and 51 deletions

5
.gitignore vendored
View File

@ -36,4 +36,7 @@ __pycache__
.pytest_cache
### Parser ###
parser/
parser/
envs.sh

View File

@ -1,4 +1,4 @@
## Start aplikacji
# Start aplikacji
Zeby wystartowac aplikacje backendu najpierw nalezy postawic testowa baze danych na naszym komputerze za pomoca dockera. Jesli raz juz go odpalimy przy nastepnym razem bardzo mozliwe, ze wlaczy sie sam. AAby sprawdzic czy docker jesty wystartowany mozna uzyc `docker ps`
@ -13,7 +13,7 @@ cd restservice
mvn spring-boot:run
```
## Token obtaining
# Token obtaining
Żeby tesotwać API wpełni potrzebny nam jest token który otrzymujemy na podstawie ticketa z systemu autoryzacyjnego **CAS**. Z tego powodu system autoryzacji działa inaczej niż w "książkowych" restowych aplikacjach i np Postman za nas jej nie dokona. Musimy mu podać już uzyskany token. Aby łatwo go uzyskać odpal skrypt
@ -23,7 +23,7 @@ python gettoken.py
Na koniec w przęglądarce dostaniesz w odpowiedzi token. W samym pliku można zmienić porty aplikacji jeśli to potrzebne.
## Profiles
# Profiles
W aplikacji posiadamy dwa profile. `dev` i `prod`. **Dev** używamy do testowania aplikacji lokalnie. **Pord** służy do stworzenia builda na produkcję.
Profil wybieramy w pliku `restservice/src/main/resources/application.properties` wpisując odpowiednią nazwę
@ -45,8 +45,12 @@ W paczce dla proda w protpertiesach poufne dane odczytywane są ze zmiennych śr
- `PLANNAPLAN_EMAIL_USERNAME` - login naszego maila
- `PLANNAPLAN_EMAIL_PASSWORD` - hasło naszego maila
- `PLANNAPLAN_EMAIL` - nasz adres maila
- `PLANNAPLAN_CONSUMER_KEY` - nasz klucz do usos api
- `PLANNAPLAN_CONSUMER_SECRET` - secret naszego klucza
## Packaging
Należy też pamiętać, że zmienne `PLANNAPLAN_CONSUMER_KEY` oraz `PLANNAPLAN_CONSUMER_SECRET` są potrzebne również w profilu `dev` więc trzeba je dodać w celu tesotowania do zmiennych we własnym systemie
# Packaging
Zeby spakowac apke do `jara` wystarcza dwie komendy zaczynajac z glownego katalogu projektu
@ -56,23 +60,23 @@ mvn clean; mvn install; cd restservice; mvn clean package spring-boot:repackage
Utworzony zostanie jar w `restservice/target/restservice-1.0-SNAPSHOT.jar`. Oczywiscie zeby jar zadzialal kontenery dockerowe musza byc odpalone (lub baza danych na serwerze jesli zmienialismy propertisy z localhost)
## Generowanie dokumentacji
# Generowanie dokumentacji
### Javadocs
## Javadocs
```bash
mvn javadoc:javadoc
```
### Api docs
## Api docs
Żeby zobaczyć dokumentację api trzeba wejść w przeglądarce na `http://localhost:1285/swagger-ui.html` po odpaleniu aplikacji.
#### Nazewnictwo odpowiedzi
### Nazewnictwo odpowiedzi
Każdą odpowiedź zaczynamy od modelu, który opisuje np. `Courses` a kończymy na `Response`. Między tymi dwoma członami możemy dodawać modyfikatory opisujące dokładniej odpowiedź np. `Default`. W ten sposób możemy otrzymać nazwę `CoursesDefaultResponse.java`
## Troubleshooting
# Troubleshooting
Spring chyba cacheuje jakies dane dotyczace polaczenia wiec jesli spring wywali Ci blad `Connection Refused`, a wiesz, ze ta baza stoi na podanym ip i porcie to sprobuj
@ -80,3 +84,28 @@ Spring chyba cacheuje jakies dane dotyczace polaczenia wiec jesli spring wywali
mvn clean
mvn install
```
Jeżeli używasz VSCode i testy, które wymagają podanych zmiennych środowiskowych (testy odpytywania usos api) trzeba podać te zmienne w pliku `.vscode/settings.json`
```json
{
"files.exclude": {
"**/.classpath": true,
"**/.project": true,
"**/.settings": true,
"**/.factorypath": true
},
"java.configuration.updateBuildConfiguration": "disabled",
"java.format.settings.url": "eclipse-formatter.xml",
"java.test.config": [
{
"name": "myConfiguration",
"workingDirectory": "${workspaceFolder}",
"env": {
"PLANNAPLAN_CONSUMER_KEY": "value",
"PLANNAPLAN_CONSUMER_SECRET": "value"
}
}
]
}
```

View File

@ -74,6 +74,31 @@
<version>2.2.5.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.social/spring-social-core -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.scribejava</groupId>
<artifactId>scribejava-core</artifactId>
<version>8.1.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
</dependencies>
<build>

View File

@ -4,6 +4,21 @@
"name": "plannaplan.email",
"type": "java.lang.String",
"description": "Email from which app sends message"
},
{
"name": "plannaplan.apiurl",
"type": "java.lang.String",
"description": "Url to usos api endpoints"
},
{
"name": "plannaplan.apikey",
"type": "java.lang.String",
"description": "Api consumer key"
},
{
"name": "plannaplan.apisecret",
"type": "java.lang.String",
"description": "Api consumer secret"
}
]
}

View File

@ -0,0 +1,61 @@
package com.plannaplan.api;
import com.github.scribejava.core.builder.api.DefaultApi10a;
/**
* singleton class to sign usos api requests with oauth
*/
public class UsosOauth1Service extends DefaultApi10a {
private static final String AUTHORIZE_URL = "https://usosapidemo.amu.edu.pl/services/oauth/authorize";
private static final String REQUEST_TOKEN_URL = "https://usosapidemo.amu.edu.pl/services/oauth/request_token";
private final String scopesAsString;
protected UsosOauth1Service() {
scopesAsString = null;
}
protected UsosOauth1Service(String... scopes) {
final StringBuilder builder = new StringBuilder();
for (String scope : scopes) {
builder.append('+').append(scope);
}
scopesAsString = "?scope=" + builder.substring(1);
}
private static class InstanceHolder {
private static final UsosOauth1Service INSTANCE = new UsosOauth1Service();
}
public static UsosOauth1Service instance() {
return InstanceHolder.INSTANCE;
}
/**
* get instance withj scopes
*
* @param scopes to get instance with
* @return UsosOauth1Service instance
*/
public static UsosOauth1Service instance(String... scopes) {
return scopes == null || scopes.length == 0 ? instance() : new UsosOauth1Service(scopes);
}
@Override
public String getRequestTokenEndpoint() {
return scopesAsString == null ? REQUEST_TOKEN_URL : REQUEST_TOKEN_URL + scopesAsString;
}
@Override
public String getAccessTokenEndpoint() {
return "https://usosapidemo.amu.edu.pl/services/oauth/access_token";
}
@Override
protected String getAuthorizationBaseUrl() {
return AUTHORIZE_URL;
}
}

View File

@ -8,7 +8,8 @@ import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
/**
* Entity of Assignment grouping of state associated about group_id and commision_id
* Entity of Assignment grouping of state associated about group_id and
* commision_id
*
*/
@ -24,11 +25,12 @@ public class Assignment {
@JoinColumn(name = "commision_id")
private Commision commision;
private boolean isPastAssignment;
/**
* Assignment
* @param group group we would like to assign
* @param commision commision that assignment belongs to
*
* @param group group we would like to assign
* @param commision commision that assignment belongs to
* @param isPastAssignment is assignment past or no
*/
public Assignment(Groups group, Commision commision, boolean isPastAssignment) {
@ -38,7 +40,8 @@ public class Assignment {
/**
* Assignment
* @param group group we would like to assign
*
* @param group group we would like to assign
* @param commision commision that assignment belongs to
*/
public Assignment(Groups group, Commision commision) {
@ -49,8 +52,9 @@ public class Assignment {
}
/**
* Id getter
* @return id id of assignment
* Id getter
*
* @return id id of assignment
*/
public Long getId() {
@ -58,9 +62,9 @@ public class Assignment {
}
/**
* getGroup
* getGroup
*
* @return group
* @return group
*/
public Groups getGroup() {
return this.group;
@ -68,6 +72,7 @@ public class Assignment {
/**
* isPastAssignment getter
*
* @return isPastAssignment
*/
public boolean isPastAssignment() {
@ -76,7 +81,8 @@ public class Assignment {
/**
* setter isPastAssignment
* @param isPastAssignment
*
* @param isPastAssignment is assignment past or not
*/
public void setPastAssignment(boolean isPastAssignment) {
this.isPastAssignment = isPastAssignment;

View File

@ -56,7 +56,6 @@ public class Groups {
this.zajCykId = zajCykId;
}
/**
* Groups
*
@ -90,9 +89,10 @@ public class Groups {
* @param day day given to the groups
* @param lecturer lecturer given to the groups
* @param zajCykId number of class in the term
* @param gr_nr Number of class/course
* @param grNr Number of class/course
*/
public Groups(int capacity, String room, Course course, int time, int endTime, WeekDay day, Lecturer lecturer, Integer zajCykId, Integer grNr) {
public Groups(int capacity, String room, Course course, int time, int endTime, WeekDay day, Lecturer lecturer,
Integer zajCykId, Integer grNr) {
this(capacity, room, course, time, endTime, day, lecturer);
this.zajCykId = zajCykId;
this.grNr = grNr;
@ -108,9 +108,10 @@ public class Groups {
* @param day day given to the groups
* @param lecturer lecturer given to the groups
* @param zajCykId number of class in the term
* @param grNr Number of class/course
* @param grNr Number of class/course
*/
public Groups(int capacity, String room, Course course, int time, WeekDay day, Lecturer lecturer, Integer zajCykId, Integer grNr) {
public Groups(int capacity, String room, Course course, int time, WeekDay day, Lecturer lecturer, Integer zajCykId,
Integer grNr) {
this(capacity, room, course, time, time + DEFAULT_CLASS_TIME, day, lecturer);
this.zajCykId = zajCykId;
this.grNr = grNr;
@ -141,34 +142,35 @@ public class Groups {
* @param day day given to the groups
* @param lecturer lecturer given to the groups
*/
public void update(Integer capacity, String room, Course course, Integer time, Integer endTime, WeekDay day, Lecturer lecturer){
if (capacity != null){
public void update(Integer capacity, String room, Course course, Integer time, Integer endTime, WeekDay day,
Lecturer lecturer) {
if (capacity != null) {
this.capacity = capacity;
}
}
if (room != null){
if (room != null) {
this.room = room;
}
if (course != null){
}
if (course != null) {
this.course = course;
}
}
if (time != null){
if (time != null) {
this.time = time;
}
}
if (endTime != null){
if (endTime != null) {
this.endTime = endTime;
}
}
if (day != null){
if (day != null) {
this.day = day;
}
}
if (lecturer != null){
if (lecturer != null) {
this.lecturer = lecturer;
}
}
}
/**

View File

@ -9,6 +9,7 @@ import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import com.plannaplan.models.UserApiResponse;
import com.plannaplan.types.UserRoles;
/**
@ -40,7 +41,7 @@ public class User {
* @param name name given to the user
* @param surname surname given to the user
* @param mail mail given to the user
* @param role
* @param role user's role
*/
public User(String name, String surname, String mail, UserRoles role) {
this.name = name;
@ -55,7 +56,7 @@ public class User {
* @param surname surname given to the user
* @param mail mail given to the user
* @param usosId id in the USOS system
* @param role
* @param role user's role
*/
public User(String name, String surname, String mail, String usosId, UserRoles role) {
this(name, surname, mail, role);
@ -188,6 +189,16 @@ public class User {
return this.id;
}
/**
* updates user entity with data got by UsosApiService::getUserData
*
* @param usosData UserApiResponse model with needed data
*/
public void updateWithUsosData(UserApiResponse usosData) {
this.name = usosData.getName();
this.surname = usosData.getSurname();
}
/**
* it checks if given ammount of time passed since last token usage. If not
* retunr true and reset time otherwise return false and token won work anymore

View File

@ -0,0 +1,29 @@
package com.plannaplan.models;
/**
* Model to keep data from /services/users/user response called in
* UsosApiService
*/
public class UserApiResponse {
private String name;
private String surname;
public UserApiResponse() {
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@ -36,6 +36,12 @@ import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("FROM User WHERE email = ?1 OR usosId = ?1")
/**
* return user by given authority
*
* @param authority user usosId or email
* @return optional with user if found
*/
Optional<User> getByAuthority(@Param("authority") String authority);
@Query("FROM User WHERE email = ?1")
@ -51,6 +57,13 @@ public interface UserRepository extends JpaRepository<User, Long> {
List<User> searchForUsers(@Param("query") String query);
@Query("FROM User WHERE (name LIKE %?1% OR surname LIKE %?1%) AND role=?2")
/**
* search for user with given query
*
* @param query string that will be matched to users name and surname
* @param role limits results by role
* @return list opf results
*/
List<User> searchForUsers(@Param("query") String query, @Param("role") UserRoles role);
@Query("FROM User WHERE role=?1")

View File

@ -67,9 +67,9 @@ public class GroupService {
/**
*
* @param assingemnts list of assingemnts you want to get taken places ammount
* @return HashMap<Long, Integer> where Long is group id and Integer is how many
* places in gorup is already taken
* @param assignments list of assignments you want to get taken places ammount
* @return HashMap of Long to Integer where Long is group id and Integer is how
* many places in gorup is already taken
*/
public HashMap<Long, Integer> getTakenPlacesOfAssignments(List<Assignment> assignments) {
return getTakenPlaces(assignments.stream().map(Assignment::getGroup).collect(Collectors.toList()));
@ -78,8 +78,8 @@ public class GroupService {
/**
*
* @param groups list of groups you want to get taken places ammount
* @return HashMap<Long, Integer> where Long is group id and Integer is how many
* places in gorup is already taken
* @return HashMap of Long to Integer where Long is group id and Integer is how
* many places in gorup is already taken
*/
public HashMap<Long, Integer> getTakenPlaces(List<Groups> groups) {
HashMap<Long, Integer> response = new HashMap<>();

View File

@ -6,6 +6,7 @@ import java.util.UUID;
import com.plannaplan.entities.User;
import com.plannaplan.exceptions.UserNotFoundException;
import com.plannaplan.models.UserApiResponse;
import com.plannaplan.repositories.UserRepository;
import com.plannaplan.types.UserRoles;
@ -20,14 +21,33 @@ public class UserService {
@Autowired
private UserRepository repo;
@Autowired
private UsosApiService service;
public UserService() {
super();
}
/**
* checks if user exist and return him or creates new one with student role
* otherwise
*
* @param email user email in usos
* @param usosId user id in usos
* @return user entity instace containing changes saved in database
*/
public User checkForUser(String email, String usosId) {
return this.checkForUser(email, usosId, UserRoles.STUDENT);
}
/**
* checks if user exist and creates new one if doesn't
*
* @param email user email in usos
* @param usosId user id in usos
* @param roleIfNotExist role to be set in case user is not in database yet
* @return user entity instace containing changes saved in database
*/
public User checkForUser(String email, String usosId, UserRoles roleIfNotExist) {
if (usosId == null) {
Optional<User> user = this.repo.getByEmail(email.replace("\n", "").trim());
@ -48,8 +68,20 @@ public class UserService {
}
}
/**
* generates token for user and if user don't have name in database it will
* attemp to obtain these from usos api and saves changes in database
*
* @param authority user we want to login
* @return user with changed values after save in db
* @throws UserNotFoundException throwed if user doesn't exist
*/
public User login(User authority) throws UserNotFoundException {
final String token = UUID.randomUUID().toString();
if ((authority.getName() == null || authority.getSurname() == null) && authority.getUsosId() != null) {
final UserApiResponse resp = this.service.getUserData(authority.getUsosId());
authority.updateWithUsosData(resp);
}
try {
authority.setToken(token);
this.repo.save(authority);
@ -59,16 +91,34 @@ public class UserService {
return authority;
}
/**
* sacves user to databse and return instatnce with id
*
* @param user to be saved
* @return instatnce with bd id
*/
public User save(User user) {
return this.repo.save(user);
}
/**
*
* @param email of user to be find
* @return user with given mail
* @throws UserNotFoundException throwed if user doesn't exist
*/
public User getUserByEmail(String email) throws UserNotFoundException {
return this.repo.getByEmail(email.replace("\n", "").trim())
.orElseThrow(() -> new UserNotFoundException("Cannot find user with given authority"));
}
/**
* return user by given authority
*
* @param authority user usosId or email
* @return optional with user if found
*/
public Optional<User> getByAuthority(String authority) {
return this.repo.getByAuthority(authority);
}
@ -77,6 +127,12 @@ public class UserService {
return this.repo.getByToken(token);
}
/**
* search for user with given query
*
* @param query string that will be matched to users name and surname
* @return list opf results
*/
public List<User> searchForStudents(String query) {
return this.repo.searchForUsers(query, UserRoles.STUDENT);
}
@ -93,8 +149,8 @@ public class UserService {
return this.repo.getByRefreshToken(refreshToken);
}
public boolean adminExists(){
public boolean adminExists() {
return this.repo.getAllByRole(UserRoles.ADMIN).size() > 0;
}
}
}

View File

@ -0,0 +1,74 @@
package com.plannaplan.services;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.github.scribejava.core.builder.ServiceBuilder;
import com.github.scribejava.core.model.OAuth1AccessToken;
import com.github.scribejava.core.model.OAuthRequest;
import com.github.scribejava.core.model.Response;
import com.github.scribejava.core.model.Verb;
import com.github.scribejava.core.oauth.OAuth10aService;
import com.plannaplan.api.UsosOauth1Service;
import com.plannaplan.models.UserApiResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* service to call usos api endpoints
*/
@Service
public class UsosApiService {
private static final String NAME_FIELD = "first_name";
private static final String SURNAME_FIELD = "last_name";
@Value("${plannaplan.apiurl}")
private String apiUrl;
@Value("${plannaplan.apikey}")
private String apikey;
@Value("${plannaplan.apisecret}")
private String apisecret;
public UsosApiService() {
}
/**
* /services/users/user
*
* @param usosId user id in usos
* @return UserApiResponse modle contatining desired values
*/
public UserApiResponse getUserData(String usosId) {
final UserApiResponse apiResponse = new UserApiResponse();
try {
final OAuth10aService service = new ServiceBuilder(apikey).apiSecret(apisecret)
.build(UsosOauth1Service.instance());
final OAuthRequest request = new OAuthRequest(Verb.GET, apiUrl + "/services/users/user?user_id=" + usosId);
service.signRequest(new OAuth1AccessToken("", ""), request);
try (Response response = service.execute(request)) {
final String json = response.getBody();
final ObjectMapper mapper = new ObjectMapper();
Map<String, String> map = mapper.readValue(json, new TypeReference<Map<String, String>>() {
});
apiResponse.setName(map.get(NAME_FIELD));
apiResponse.setSurname(map.get(SURNAME_FIELD));
}
} catch (IOException | InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return apiResponse;
}
}

View File

@ -0,0 +1,38 @@
package com.plannaplan.services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import com.plannaplan.models.UserApiResponse;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration
public class UsosApiServiceTest {
@Autowired
private UsosApiService service;
@Test
@Ignore
public void shouldReturnPersonalData() throws IOException, InterruptedException, ExecutionException {
final UserApiResponse response = this.service.getUserData("499054");
assertTrue(response.getName().equals("Marcin"));
assertTrue(response.getSurname().equals("Woźniak"));
}
}

View File

@ -14,5 +14,8 @@ spring.mail.properties.mail.smtp.auth=false
spring.mail.properties.mail.smtp.starttls.enable=false
plannaplan.email = plannaplan.kontakt@gmail.com
plannaplan.apiurl = https://usosapidemo.amu.edu.pl
plannaplan.apikey=${PLANNAPLAN_CONSUMER_KEY}
plannaplan.apisecret=${PLANNAPLAN_CONSUMER_SECRET}
server.port=1285

View File

@ -18,4 +18,7 @@ logging.level.io.swagger.models.parameters.AbstractSerializableParameter=ERROR
server.port=1285
plannaplan.dev = true
plannaplan.frontendUrl = http://localhost:3000
plannaplan.email = plannaplan.kontakt@gmail.com
plannaplan.email = plannaplan.kontakt@gmail.com
plannaplan.apiurl = https://usosapidemo.amu.edu.pl
plannaplan.apikey=${PLANNAPLAN_CONSUMER_KEY}
plannaplan.apisecret=${PLANNAPLAN_CONSUMER_SECRET}

View File

@ -20,6 +20,10 @@ server.port=1285
plannaplan.email = ${PLANNAPLAN_EMAIL}
plannaplan.dev = false
plannaplan.frontendUrl= https://wmi.plannaplan.pl
plannaplan.apiurl = https://usosapidemo.amu.edu.pl
plannaplan.apikey=${PLANNAPLAN_CONSUMER_KEY}
plannaplan.apisecret=${PLANNAPLAN_CONSUMER_SECRET}
security.require-ssl=true
server.ssl.key-store=/keys/keystore.p12
server.ssl.key-store-password=