Compare commits

...

9 Commits

9 changed files with 391 additions and 40 deletions

30
src/SECURITY.md Normal file
View File

@ -0,0 +1,30 @@
<!-- AUTO-GENERATED, DO NOT EDIT! -->
<!-- Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/SECURITY.md -->
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Security Policy](#security-policy)
- [Supported Versions](#supported-versions)
- [Reporting a Vulnerability](#reporting-a-vulnerability)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
# Security Policy
## Supported Versions
We release patches for security vulnerabilities. Which versions are eligible for
receiving such patches depends on the CVSS v3.0 Rating:
| CVSS v3.0 | Supported Versions |
| --------- | ----------------------------------------- |
| 9.0-10.0 | Releases within the previous three months |
| 4.0-8.9 | Most recent release |
## Reporting a Vulnerability
Please report (suspected) security vulnerabilities to
**[security@ory.sh](mailto:security@ory.sh)**. You will receive a response from
us within 48 hours. If the issue is confirmed, we will release a patch as soon
as possible depending on complexity but historically within a few days.

View File

@ -2,54 +2,50 @@
namespace App\Models\Traits;
use Illuminate\Database\Eloquent\Casts\Attribute;
trait FormattedPhoneTrait
{
/**
* Format a phone number to be human readable.
*
* @since 1.0.0
*
* @return string
*/
public function getPhoneNumberAttribute(): string
{
$phoneLength = strlen($this->phone);
$phoneNumber = preg_replace('//', '', $this->phone);
if ($phoneLength > 10) {
$countryCode = substr($phoneNumber, 0, $phoneLength - 10);
$areaCode = substr($phoneNumber, -10, 3);
$nextThree = substr($phoneNumber, -7, 3);
$lastFour = substr($phoneNumber, -4, 4);
$phoneNumber = "({$areaCode}) {$nextThree}-{$lastFour}";
} elseif ($phoneLength == 10) {
$areaCode = substr($phoneNumber, 0, 3);
$nextThree = substr($phoneNumber, 3, 3);
$lastFour = substr($phoneNumber, 6, 4);
$phoneNumber = "({$areaCode}) {$nextThree}-{$lastFour}";
} elseif ($phoneLength == 7) {
$nextThree = substr($phoneNumber, 0, 3);
$lastFour = substr($phoneNumber, 3, 4);
$phoneNumber = "{$nextThree}-{$lastFour}";
}
return $phoneNumber;
}
/**
* Remove all non-numeric characters from the phone number.
*
* @since 1.0.0
*
* @param string $value
* @package Namespace\App\Contact
* @since 1.0.0
*
* @return void
*/
public function setPhoneNumberAttribute($value): void
protected function phoneNumber(): Attribute
{
$this->attributes['phone'] = preg_replace('/[^0-9]/', '', $value);
return Attribute::make(
get: function (mixed $value, array $attributes) {
$phoneLength = strlen($attributes['phone_number']);
$phoneNumber = preg_replace('/[^0-9]/', '', $attributes['phone_number']);
if ($phoneLength > 10) {
$countryCode = substr($phoneNumber, 0, $phoneLength - 10);
$areaCode = substr($phoneNumber, -10, 3);
$nextThree = substr($phoneNumber, -7, 3);
$lastFour = substr($phoneNumber, -4, 4);
$phoneNumber = "({$areaCode}) {$nextThree}-{$lastFour}";
} elseif ($phoneLength == 10) {
$areaCode = substr($phoneNumber, 0, 3);
$nextThree = substr($phoneNumber, 3, 3);
$lastFour = substr($phoneNumber, 6, 4);
$phoneNumber = "({$areaCode}) {$nextThree}-{$lastFour}";
} elseif ($phoneLength == 7) {
$nextThree = substr($phoneNumber, 0, 3);
$lastFour = substr($phoneNumber, 3, 4);
$phoneNumber = "{$nextThree}-{$lastFour}";
}
return $phoneNumber;
},
set: function (mixed $value) {
return preg_replace('/[^0-9]/', '', $value);
},
);
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace Laravel\Jetstream;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
trait HasProfilePhoto
{
/**
* Update the user's profile photo.
*
* @param \Illuminate\Http\UploadedFile $photo
* @param string $storagePath
* @return void
*/
public function updateProfilePhoto(UploadedFile $photo, $storagePath = 'profile-photos')
{
tap($this->profile_photo_path, function ($previous) use ($photo, $storagePath) {
$this->forceFill([
'profile_photo_path' => $photo->storePublicly(
$storagePath, ['disk' => $this->profilePhotoDisk()]
),
])->save();
if ($previous) {
Storage::disk($this->profilePhotoDisk())->delete($previous);
}
});
}
/**
* Delete the user's profile photo.
*
* @return void
*/
public function deleteProfilePhoto()
{
if (! Features::managesProfilePhotos()) {
return;
}
if (is_null($this->profile_photo_path)) {
return;
}
Storage::disk($this->profilePhotoDisk())->delete($this->profile_photo_path);
$this->forceFill([
'profile_photo_path' => null,
])->save();
}
/**
* Get the URL to the user's profile photo.
*
* @return \Illuminate\Database\Eloquent\Casts\Attribute
*/
public function profilePhotoUrl(): Attribute
{
return Attribute::get(function () {
return $this->profile_photo_path
? Storage::disk($this->profilePhotoDisk())->url($this->profile_photo_path)
: $this->defaultProfilePhotoUrl();
});
}
/**
* Get the default profile photo URL if no profile photo has been uploaded.
*
* @return string
*/
protected function defaultProfilePhotoUrl()
{
return 'https://avatars.test/avatar?size=256';
}
/**
* Get the disk that profile photos should be stored on.
*
* @return string
*/
protected function profilePhotoDisk()
{
return isset($_ENV['VAPOR_ARTIFACT_NAME']) ? 's3' : config('jetstream.profile_photo_disk', 'public');
}
}

View File

@ -30,6 +30,9 @@
"torann/geoip": "", // support for multiple GeoIP services
"coderjerk/nasa-php": "", // NASA API integrations
// File management
"czim/laravel-paperclip": "", // attach files to an Eloquent model
// Media (image/video/audio) Management
"intervention/image": "", // image manipulation
"intervention/imagecache": "", // caching for intervention/image
@ -37,6 +40,7 @@
"spatie/laravel-image-optimizer": "", // optimize png, jpg, svg, and gif
"unisharp/laravel-filemanager": "", // File manager (including uploads)
"pbmedia/laravel-ffmpeg": "", // Integration with FFMpeg
"maestroerror/php-heic-to-jpg": "", // Use Go(?) to convert HEIC/HEIF to JPEG
// Localization
"mcamara/laravel-localization": "", // localization that also handles route translations

View File

@ -6,6 +6,7 @@ use App\Models\Language;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Laravel\Jetstream\Features;
@ -71,4 +72,25 @@ class UserFactory extends Factory
'ownedTeams'
);
}
/**
* Indicate that the user should have an avatar image.
*
* @return $this
*/
public function withAvatar()
{
if (! Features::enabled(Features::profilePhotos())) {
return $this->state([]);
}
$imageUrl = 'https://avatars.test/';
$imageContents = file_get_contents($imageUrl);
$imageName = md5($imageContents) . '.jpg';
Storage::put("public/avatars/{$imageName}", $imageContents);
return $this->state([
'profile_photo_path' => "avatars/{$imageName}"
]);
}
}

View File

@ -26,7 +26,30 @@
@import 'components/navbars.css';
@import 'components/pagination.css';
:root {
--primary-text-color: hsl(0, 90.4%, 59.2%);
--primary-text-color-gradient-start: hsl(270, 66.9%, 47.5%);
--primary-text-color-gradient-end: hsl(330, 100%, 50%);
--text-info-color: hsl(191, 82%, 50%);
--text-info-color-gradient-start: hsl(227, 100%, 56.5%);
--text-info-color-gradient-end: hsl(191, 98.2%, 56.1%);
--text-success-color: hsl(86, 81.4%, 46.3%);
--text-success-color-gradient-start: hsl(133, 76.5%, 38.4%);
--text-success-color-gradient-end: hsl(72, 81.1%, 52.4%);
--text-warning-color: hsl(47, 96.2%, 59.2%);
--text-warning-color-gradient-start: hsl(0, 90.4%, 59.2%);
--text-warning-color-gradient-end: hsl(47, 96.2%, 59.2%);
--text-danger-color: hsl(0, 95%, 47.1%);
--text-danger-color-gradient-start: hsl(0, 92.8%, 43.5%);
--text-danger-color-gradient-end: hsl(344, 100%, 70%);
}
body {
background-image: linear-gradient(100deg, hsl(0, 0%, 98%), hsl(240, 5.9%, 90%));
color: hsl(240, 5.9%, 10%);
/*color: lch(8.35%, 2.25, 285.92);*/
-webkit-font-smoothing: antialiased;
@ -50,6 +73,16 @@ nav {
user-select: none;
}
img {
max-width: 100%;
height: auto;
vertical-align: middle;
font-style: italic;
background-repeat: no-repeat;
background-size: cover;
shape-margin: 0.75rem;
}
@media (max-width: 1023px) {
.grid-container {
@apply grid-cols-1;

View File

@ -0,0 +1,106 @@
.container {
display: grid;
gap: 4em 1em;
}
.primary-button {
--h: 196;
--s: 100%;
--l: 41%;
}
.secondary-button {
--h: 28;
--s: 100%;
--l: 41%;
}
.glass-button {
font-size: clamp(1.2rem, 5vw + 1rem, 2.5rem);
width: 10em;
height: 4em;
border-radius: 0.5em;
background-image: linear-gradient(#0003,#0000);
box-shadow:
0 -0.125em 0.25em #0002,
0 0.25em 0.25em hsl(var(--h) var(--s) var(--l) / 0.5),
0 0 0 0.1em hsl(var(--h) var(--s) var(--l) / 0.5),
0 0.175em 0.3em hsl(var(--h) var(--s) var(--l) / 0.5) inset,
0 -0.025em 0.175em hsl(var(--h) var(--s) var(--l) / 0.4) inset,
0 -0.25em 1em 0.3em hsl(var(--h) var(--s) var(--l) / 0.3) inset,
0 0.6em 0 hsl(var(--h) var(--s) var(--l) / 0.3) inset,
0 2em 1em #0004;
backdrop-filter: blur(0.15em);
position: relative;
display: grid;
place-content: center;
color: hsl(var(--h) var(--s) var(--l) / .7);
text-shadow:
0.03em 0.03em #fff5,
-0.03em -0.03em #0005;
cursor: pointer;
transition: 0.1s ease;
padding-top: 0.2em;
border: none;
background: transparent;
}
.glass-button:before {
content: '';
height: 1.5em;
left: 10%;
top: 100%;
position: absolute;
width: 80%;
}
.glass-button:after {
content: '';
inset: 0;
top: 0.5em;
position: absolute;
background-image:
linear-gradient(
105deg,
transparent 20%,
hsl(var(--h) var(--s) var(--l) / .2) 20%,
hsl(var(--h) var(--s) var(--l) / .2) 30%,
transparent 30%,
transparent 32%,
hsl(var(--h) var(--s) var(--l) / .2) 5%,
hsl(var(--h) var(--s) var(--l) / .2) 40%,
transparent 0%
);
background-size: 400% 100%;
background-position: 100% 0%;
transition: .3s ease;
}
.glass-button:active:after {
background-position: -50% 0%;
}
.glass-button:active {
backdrop-filter: blur(0.08em);
box-shadow:
0 -0.125em 0.25em #0002,
0 0.25em 0.25em hsl(var(--h) var(--s) var(--l) / 0.5),
0 0 0 0.1em hsl(var(--h) var(--s) var(--l) / 0.5),
0 0.175em 0.3em hsl(var(--h) var(--s) var(--l) / 0.8) inset,
0 -0.025em 0.175em hsl(var(--h) var(--s) var(--l) / 0.4) inset,
0 -0.25em 1em 0.3em hsl(var(--h) var(--s) var(--l) / 0.3) inset,
0 0.6em 0 hsl(var(--h) var(--s) var(--l) / 0.3) inset,
0 1em 0.5em #0004;
translate: .01em .25em;
}
.glass-button:active:before {
height: 1em;
}
/*body {
background-color: #3d3d3d;
display: grid;
margin: 0;
min-height: 100vh;
place-content: center;
}*/

View File

@ -0,0 +1,45 @@
.text-gradient {
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
position: relative;
z-index: 1;
}
.text-gradient.text-primary {
background-image: linear-gradient(310deg, var(--primary-text-color-gradient-start), var(--primary-text-color-gradient-end));
}
.text-primary {
color: var(--primary-text-color);
}
.text-gradient.text-info {
background-image: linear-gradient(310deg, var(--text-info-color-gradient-start), var(--text-info-color-gradient-end));
}
.text-info {
color: var(--text-info-color);
}
.text-gradient.text-success {
background-image: linear-gradient(310deg, var(--text-success-color-gradient-start), var(--text-success-color-gradient-end));
color: hsl(86, 81.4%, 46.3%);
}
.text-success {
color: var(--text-success-color);
}
.text-gradient.text-warning {
background-image: linear-gradient(310deg, var(--text-warning-color-gradient-start), var(--text-warning-color-gradient-end));
color: hsl(47, 96.2%, 59.2%);
}
.text-warning {
color: var(--text-warning-color);
}
.text-gradient.text-danger {
background-image: linear-gradient(310deg, var(--text-danger-color-gradient-start), var(--text-danger-color-gradient-end));
color: hsl(0, 95%, 47.1%);
}
.text-danger {
color: var(--text-danger-color);
}

View File

@ -1,3 +1,30 @@
.button-row {
@apply inline-grid grid-flow-col auto-cols-max items-center; /* intentionally leaving gap out so that it can be set on per-case basis */
}
.gradBorder i {
content: '';
position: absolute;
z-index: -1;
inset: 0;
border-radius: 30px;
padding: 1.5px; /* the thickness of the border */
/* the below will do the magic */
mask:
linear-gradient(#fff 0 0) content-box,
/* this will cover only the content area (no padding) */
linear-gradient(#fff 0 0); /* this will cover all the area */
-webkit-mask-composite: xor; /* needed for old browsers until the below is more supported */
mask-composite: exclude; /* this will exclude the first layer from the second so only the padding area will be kept visible */
}
.gradBorder i::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, #ffffff73 0%, #ffffff10 50%);
transition: transform 0.7s linear;
}