Skip to content

Commit 49961c9

Browse files
committed
completed package source
1 parent a53d2d0 commit 49961c9

File tree

4 files changed

+292
-0
lines changed

4 files changed

+292
-0
lines changed

src/Otp.php

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
<?php
2+
3+
namespace PhpMonsters\Otp;
4+
5+
use Exception;
6+
use Illuminate\Support\Facades\Cache;
7+
use Illuminate\Support\Facades\Facade;
8+
use Illuminate\Support\Str;
9+
use Illuminate\Foundation\Application;
10+
11+
class Otp
12+
{
13+
private string $format = 'numeric';
14+
private string $customize;
15+
private int $length = 6;
16+
private string $separator = '-';
17+
private bool $sensitive = false;
18+
private int $expires = 15;
19+
private int $attempts = 5;
20+
private bool $repeated = true;
21+
private bool $disposable = true;
22+
private string $prefix = 'OTPPX_';
23+
private $data;
24+
private bool $skip = false;
25+
private bool $demo = false;
26+
private array $demo_passwords = ['1234', '123456', '12345678'];
27+
28+
public function __construct()
29+
{
30+
foreach (['format', 'customize', 'length', 'separator', 'sensitive', 'expires', 'attempts', 'repeated', 'disposable', 'prefix', 'data', 'demo', 'demo_passwords'] as $value) {
31+
if (!empty(config('otp.' . $value))) $this->{$value} = config('otp.' . $value);
32+
}
33+
}
34+
35+
public function __call(string $method, $params)
36+
{
37+
if (!str_starts_with($method, 'set')) {
38+
return;
39+
}
40+
41+
$property = Str::camel(substr($method, 3));
42+
if (!property_exists($this, $property)) {
43+
return;
44+
}
45+
46+
$this->{$property} = $params[0] ?? null;
47+
if ($property == 'customize') {
48+
$this->format = 'customize';
49+
}
50+
return $this;
51+
}
52+
53+
/**
54+
* @throws Exception
55+
*/
56+
public function generate(string $identifier = null, array $options = []): string
57+
{
58+
if (!empty($options)) foreach (['format', 'customize', 'length', 'separator', 'sensitive', 'expires', 'repeated', 'prefix', 'data'] as $value) {
59+
if (isset($options[$value])) $this->{$value} = $options[$value];
60+
}
61+
62+
if ($identifier === null) $identifier = session()->getId();
63+
$array = $this->repeated ? $this->readData($identifier, []) : [];
64+
$password = $this->generateNewPassword();
65+
if (!$this->sensitive) $password = strtoupper($password);
66+
$array[md5($password)] = [
67+
'expires' => time() + $this->expires * 60,
68+
'data' => $this->data
69+
];
70+
$this->writeData($identifier, $array);
71+
return $password;
72+
}
73+
74+
public function validate(string $identifier = null, string $password = null, array $options = []): object
75+
{
76+
if (!empty($options)) foreach (['attempts', 'sensitive', 'disposable', 'skip'] as $value) {
77+
if (isset($options[$value])) $this->{$value} = $options[$value];
78+
}
79+
80+
if ($password === null) {
81+
if ($identifier === null) {
82+
throw new Exception("Validate parameter can not be null");
83+
}
84+
$password = $identifier;
85+
$identifier = null;
86+
}
87+
88+
if ($this->demo && in_array($password, $this->demo_passwords)) {
89+
return (object)[
90+
'status' => true,
91+
'demo' => true
92+
];
93+
}
94+
95+
if ($identifier === null) $identifier = session()->getId();
96+
$attempt = $this->readData('_attempt_' . $identifier, 0);
97+
if ($attempt >= $this->attempts) {
98+
return (object)[
99+
'status' => false,
100+
'error' => 'max_attempt',
101+
];
102+
}
103+
104+
$codes = $this->readData($identifier, []);
105+
if (!$this->sensitive) $password = strtoupper($password);
106+
107+
if (!isset($codes[md5($password)])) {
108+
$this->writeData('_attempt_' . $identifier, $attempt + 1);
109+
return (object)[
110+
'status' => false,
111+
'error' => 'invalid',
112+
];
113+
}
114+
115+
if (time() > $codes[md5($password)]['expires']) {
116+
$this->writeData('_attempt_' . $identifier, $attempt + 1);
117+
return (object)[
118+
'status' => false,
119+
'error' => 'expired',
120+
];
121+
}
122+
123+
$response = [
124+
'status' => true,
125+
];
126+
127+
if (!empty($codes[md5($password)]['data'])) {
128+
$response['data'] = $codes[md5($password)]['data'];
129+
}
130+
131+
if (!$this->skip) $this->forget($identifier, !$this->disposable ? $password : null);
132+
$this->resetAttempt($identifier);
133+
134+
return (object)$response;
135+
}
136+
137+
public function forget(string $identifier = null, string $password = null): bool
138+
{
139+
if ($identifier === null) $identifier = session()->getId();
140+
if ($password !== null) {
141+
$codes = $this->readData($identifier, []);
142+
if (!isset($codes[md5($password)])) {
143+
return false;
144+
}
145+
unset($codes[md5($password)]);
146+
$this->writeData($identifier, $codes);
147+
return true;
148+
}
149+
150+
$this->deleteData($identifier);
151+
return true;
152+
}
153+
154+
public function resetAttempt(string $identifier = null): bool
155+
{
156+
if ($identifier === null) $identifier = session()->getId();
157+
$this->deleteData('_attempt_' . $identifier);
158+
return true;
159+
}
160+
161+
/**
162+
* @throws Exception
163+
*/
164+
private function generateNewPassword(): string
165+
{
166+
try {
167+
$formats = [
168+
'string' => $this->sensitive ? '23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ' : '23456789ABCDEFGHJKLMNPQRSTUVWXYZ',
169+
'numeric' => '0123456789',
170+
'numeric-no-zero' => '123456789',
171+
'customize' => $this->customize
172+
];
173+
174+
$lengths = is_array($this->length) ? $this->length : [$this->length];
175+
176+
$password = [];
177+
foreach ($lengths as $length) {
178+
$password[] = substr(str_shuffle(str_repeat($x = $formats[$this->format], ceil($length / strlen($x)))), 1, $length);
179+
}
180+
181+
return implode($this->separator, $password);
182+
} catch (Exception) {
183+
throw new Exception("Fail to generate password, please check the format is correct.");
184+
}
185+
}
186+
187+
private function writeData(string $key, mixed $value): void
188+
{
189+
Cache::put($this->prefix . $key, $value, $this->expires * 60 * 3);
190+
}
191+
192+
private function readData(string $key, mixed $default = null): mixed
193+
{
194+
return Cache::get($this->prefix . $key, $default);
195+
}
196+
197+
private function deleteData(string $key): void
198+
{
199+
Cache::forget($this->prefix . $key);
200+
}
201+
}

src/OtpFacade.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace PhpMonsters\Otp;
4+
5+
use Illuminate\Support\Facades\Facade;
6+
7+
class OtpFacade extends Facade
8+
{
9+
/**
10+
* Get the registered name of the component.
11+
*
12+
* @return string
13+
*/
14+
protected static function getFacadeAccessor(): string
15+
{
16+
return 'otp';
17+
}
18+
}

src/OtpServiceProvider.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace PhpMonsters\Otp;
4+
5+
use Illuminate\Support\ServiceProvider;
6+
use Illuminate\Foundation\Application;
7+
8+
class OtpServiceProvider extends ServiceProvider
9+
{
10+
/**
11+
* Register any application services.
12+
*
13+
* @return void
14+
*/
15+
public function register()
16+
{
17+
$this->app->alias(Otp::class, 'otp');
18+
}
19+
20+
/**
21+
* Bootstrap any application services.
22+
*
23+
* @return void
24+
*/
25+
public function boot()
26+
{
27+
$this->loadTranslationsFrom(__DIR__.'/../lang', 'otp');
28+
29+
$this->publishes([
30+
__DIR__.'/../resources/lang' => $this->app->langPath('vendor/otp'),
31+
__DIR__.'/../config/otp.php' => config_path('otp.php')
32+
]);
33+
}
34+
}

src/Rules/OtpValidate.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace PhpMonsters\Otp\Rules;
4+
5+
use Illuminate\Contracts\Validation\Rule;
6+
use PhpMonsters\Otp\OtpFacade as Otp;
7+
8+
class OtpValidate implements Rule
9+
{
10+
protected string $identifier;
11+
protected array $options;
12+
protected string $attribute;
13+
protected string $error;
14+
15+
public function __construct(string $identifier = null, array $options = [])
16+
{
17+
$this->identifier = $identifier ?: session()->getId();
18+
$this->options = $options;
19+
}
20+
21+
public function passes($attribute, $value): bool
22+
{
23+
$result = Otp::validate($this->identifier, $value, $this->options);
24+
if ($result->status !== true) {
25+
$this->attribute = $attribute;
26+
$this->error = $result->error;
27+
return false;
28+
}
29+
30+
return true;
31+
}
32+
33+
public function message(): string
34+
{
35+
return __('otp::messages.' . $this->error, [
36+
'attribute' => $this->attribute
37+
]);
38+
}
39+
}

0 commit comments

Comments
 (0)