Skip to content

Commit

Permalink
finish release 0.3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
DemianD committed Jul 26, 2017
2 parents 83bcedb + 8473f4b commit 8692933
Show file tree
Hide file tree
Showing 260 changed files with 6,926 additions and 908 deletions.
3 changes: 2 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
MIT License

Copyright (c) 2017 open Summer of code 2017
Copyright (c) 2017 Open Knowledge Belgium
Copyright (c) 2017 Digipolis Ghent

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,47 @@
# CODE9000
Urban Data Birds: Providing a new haven for urban biodiversity.
\#Code9000
===================
----------

\#Code9000 is a project in which Digipolis Ghent and the City of Ghent experiment with open data to create new open source proof-of-concepts. The project was developed by oSoc17. This organisation collaborates with students to create IT-related projects. Students get real cases to work on and move beyond the theoretical and we get new learnings and code to test and iterate upon.

<div align="center">
<img src="https://raw.githubusercontent.com/oSoc17/code9000/develop/web-app/src/theme/crest.png" width=300px />
</div>

For this project, we wanted to observe the common tern, a bird living near the "Houtdok" in Ghent. tbc
#### Status
<a href="https://travis-ci.org/oSoc17/code9000.svg?branch=master" >
<img src="https://camo.githubusercontent.com/3700a6394b649fb2e3620c649ae29f8ccce97be8/68747470733a2f2f7472617669732d63692e6f72672f6f536f6331372f6f617369732d66726f6e74656e642e706e67" alt="Build Status" data-canonical-src="https://travis-ci.org/oSoc17/oasis-frontend.png" style="max-width:100%;">
</a>


#### Technologies
##### API
The API handles the pictures taken by the IoT-device. We use it to collect our pictures, save it and make it accessible for other services. Since we don't have another way of validating what creature triggered the infraredsensor, we are using a human voting system. Votes are send to the API, and when an image reaches a certain treshhold, the API will assume it's validated and send it forward.

The API is made in PHP, using the Laravel framework. We chose to include user accounts as it's the only way to know for sure one person can only vote once on a picture.

##### Webapplication
To do the validation of our pictures, human validation looked like the best way (given the time we had). As a simple yes-maybe-no validation onepager seemed a little dull and unappealing, we tried to gamify it.

We made a ReactJS webapp where you can do all the account-related stuff like logging in or making an account. We made an voting page as well, and tried to implement fun features like scores, badges and a monthly leaderboard.

##### Hardware
We developed an IoT-device which takes pictures of everything that moves and send it to the API. The IoT works asynchronously to simultaneous send the pictures to the API and take pictures of the birds.

The device consists of a Raspberry Pi A+, a Raspberry Pi Camera V2 and a PIR sensor. To keep it self-sustainable we use a solar panel and a battery while a 4G router takes care of the Internet connection.
#### Contributers

##### Students
- [Demian Dekoninck](https://github.com/DemianD)
- [Diëgo De Wilde](https://github.com/diegodewilde)
- [Dylan Van Assche](https://github.com/DylanVanAssche)
- [Bert Commeine](https://github.com/BertCommeine)
- [Cynthia Vanoirbeek](https://github.com/cynthiav11)

##### Coaches
- [Miet Claes](https://miet.be)
- [Xavier Bertels](https://mono.company)

#### MIT License
This project is released as an open-source project under the <a href="https://github.com/oSoc17/code9000/blob/develop/LICENSE"> MIT License </a>
30 changes: 27 additions & 3 deletions api/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
APP_NAME=Laravel
APP_NAME=birds.today
APP_ENV=local
APP_KEY=
APP_DEBUG=true
Expand All @@ -16,7 +16,7 @@ DB_PASSWORD=secret
BROADCAST_DRIVER=log
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync
QUEUE_DRIVER=database

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
Expand All @@ -29,11 +29,35 @@ MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

MAILGUN_DOMAIN=
MAILGUN_SECRET=

MAIL_FROM_ADDRESS=
MAIL_FROM_NAME=

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=

VALID_OBSERVATION_THRESHOLD=5
UNVALID_OBSERVATION_THRESHOLD=-5

PASSWORD_RESET_MINUTES=30

DEPLOY_BRANCH_WEBHOOK=master

FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=
FACEBOOK_CLIENT_SECRET=
FACEBOOK_PAGE_ID=
FACEBOOK_PAGE_TOKEN=

TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_TOKEN_SECRET=

IMGUR_CLIENT_ID

DATAHUBGENT_API_URL=
DATAHUBGENT_PUBLIC_HASH=
DATAHUBGENT_PRIVATE_HASH=
1 change: 1 addition & 0 deletions api/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ Homestead.json
Homestead.yaml
npm-debug.log
.env
/public/build
26 changes: 26 additions & 0 deletions api/app/Events/ObservationIsValid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace App\Events;

use App\Observation;
use Illuminate\Queue\SerializesModels;

class ObservationIsValid
{
use SerializesModels;

/**
* @var \App\Observation
*/
public $observation;

/**
* Create a new event instance.
*
* @param \App\Observation $observation
*/
public function __construct(Observation $observation)
{
$this->observation = $observation;
}
}
26 changes: 26 additions & 0 deletions api/app/Events/ObservationUploadedToImgur.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace App\Events;

use App\Observation;
use Illuminate\Queue\SerializesModels;

class ObservationUploadedToImgur
{
use SerializesModels;

/**
* @var \App\Observation
*/
public $observation;

/**
* Create a new event instance.
*
* @param \App\Observation $observation
*/
public function __construct(Observation $observation)
{
$this->observation = $observation;
}
}
2 changes: 1 addition & 1 deletion api/app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public function render($request, Exception $exception)
if ($exception instanceof NotFoundHttpException && ! strpos($request->url(), '/api')) {
Log::info('NotFoundHttpException, Route not found, serving index.html of build folder');

return new Response(File::get(public_path().'/build/index.html'), Response::HTTP_OK);
return new Response(File::get(public_path().'/index.html'), Response::HTTP_OK);
}

return parent::render($request, $exception);
Expand Down
24 changes: 16 additions & 8 deletions api/app/Http/Controllers/Api/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@
use Illuminate\Http\Request;
use Tymon\JWTAuth\Facades\JWTAuth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\UserLogin;
use App\Http\Requests\Api\UserRegistrationModel;
use Tymon\JWTAuth\Exceptions\TokenBlacklistedException;

class AuthController extends Controller
{
/**
* Authenticate the user and create a token.
*
* @param \Illuminate\Http\Request $request
* @param $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function auth(Request $request)
public function auth(UserLogin $request)
{
$credentials = $request->only('email', 'password');

Expand Down Expand Up @@ -49,10 +51,14 @@ public function me(Request $request)
*/
public function refresh()
{
$token = JWTAuth::getToken();
$newToken = JWTAuth::refresh($token);
try {
$token = JWTAuth::getToken();
$newToken = JWTAuth::refresh($token);

return response()->json(compact('newToken'));
return response()->json(compact('newToken'));
} catch (TokenBlacklistedException $exception) {
return response()->json(['error' => 'token_blacklisted'], $exception->getStatusCode());
}
}

public function logout()
Expand All @@ -69,12 +75,14 @@ public function logout()
*/
public function register(UserRegistrationModel $request)
{
$account = [
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt($request->password),
];
]);

User::create($account);
$token = JWTAuth::fromUser($user);

return response()->json(compact('token'));
}
}
2 changes: 1 addition & 1 deletion api/app/Http/Controllers/Api/AuthSocialiteController.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function handleProviderCallback()

private function firstOrCreateUser($facebookUser)
{
return User::firstOrCreate(['email' => $facebookUser->email], ['name' => $facebookUser->name]);
return User::updateOrCreate(['email' => $facebookUser->email], ['name' => $facebookUser->name, 'avatar_url' => $facebookUser->avatar]);
}

private function firstOrCreateProvider($user, $facebookUser)
Expand Down
2 changes: 1 addition & 1 deletion api/app/Http/Controllers/Api/GithubWebhookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public function deploy(Request $request)

abort_unless($ref === $payload->ref, 403);

$response = Artisan::call('deploy');
Artisan::queue('deploy');

return response(['success' => true]);
}
Expand Down
21 changes: 21 additions & 0 deletions api/app/Http/Controllers/Api/LeaderboardController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Services\Leaderboard\Leaderboard;

class LeaderboardController extends Controller
{
private $leaderboardService;

public function __construct(Leaderboard $leaderboardService)
{
$this->leaderboardService = $leaderboardService;
}

public function index()
{
return $this->leaderboardService->forAllUsers();
}
}
3 changes: 1 addition & 2 deletions api/app/Http/Controllers/Api/ObservationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ public function index()

public function forUser()
{
// TODO: only get observations that aren't validated
return Observation::whereDoesntHave('votes', function ($query) {
return Observation::where('is_valid', null)->whereDoesntHave('votes', function ($query) {
$query->where('user_id', auth()->user()->id);
})
->orderBy('captured_at', 'ASC') // TODO: change to caputred_at
Expand Down
98 changes: 98 additions & 0 deletions api/app/Http/Controllers/Api/PasswordResetController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

namespace App\Http\Controllers\Api;

use Mail;
use App\User;
use Carbon\Carbon;
use App\PasswordReset;
use App\Mail\PasswordResetMail;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\NewPasswordModel;
use App\Http\Requests\Api\PasswordResetModel;

class PasswordResetController extends Controller
{
private $passwordResetMinutes;

public function __construct()
{
$this->passwordResetMinutes = config('app.password_reset_minutes');
}

/**
* Send a mail for resetting the password.
*
* @param \App\Http\Requests\Api\PasswordResetModel $request
*
* @return mixed
*/
public function sendResetMail(PasswordResetModel $request)
{
$userEmail = $request->email;

$user = User::where('email', $userEmail)->first();

if ($user && ! $this->isSpamming($user)) {
$token = str_random(150);

$this->sendPasswordResetMail([
'email' => $user->email,
'url' => sprintf('%s/reset-password/%s', config('app.url_front_end'), $token), // Redirect to front-end
'name' => $user->name,
]);

PasswordReset::create([
'user_id' => $user->id,
'token' => $token,
'created_at' => Carbon::now(),
]);
}
}

private function isSpamming(User $user)
{
$lastPasswordReset = $user->passwordResets()->first();

if (! $lastPasswordReset) {
return false;
}

return $this->isInsideInterval($lastPasswordReset->created_at);
}

/**
* Store the new password in the database if token is valid and time <
* app.password_reset_minutes.
*
* @param \App\Http\Requests\Api\NewPasswordModel $request
* @param string $token
*
* @return \Illuminate\Http\JsonResponse
*/
public function resetPassword(NewPasswordModel $request, $token)
{
$passwordReset = PasswordReset::with('user')->where('token', $token)->first();

if ($passwordReset && $this->isInsideInterval($passwordReset->created_at)) {
$passwordReset->user->password = bcrypt($request->password);
$passwordReset->user->save();

$passwordReset->delete();

return response()->json(['success' => 'ok']);
}

return response()->json(['error' => 'invalid'], 404);
}

public function sendPasswordResetMail($mailData)
{
Mail::to($mailData['email'])->send(new PasswordResetMail($mailData));
}

private function isInsideInterval($passwordResetDate)
{
return Carbon::now()->diffInMinutes($passwordResetDate) <= $this->passwordResetMinutes;
}
}
Loading

0 comments on commit 8692933

Please sign in to comment.