Procházet zdrojové kódy

Initial commit

master
Immanuel Onyeka před 3 roky
revize
0acfbd61d1
5 změnil soubory, kde provedl 1159 přidání a 0 odebrání
  1. +1
    -0
      .gitignore
  2. +278
    -0
      BillingController.php
  3. +6
    -0
      README.md
  4. +745
    -0
      main.scss
  5. +129
    -0
      panel.vue

+ 1
- 0
.gitignore Zobrazit soubor

@@ -0,0 +1 @@
*.sw?

+ 278
- 0
BillingController.php Zobrazit soubor

@@ -0,0 +1,278 @@
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Stripe\Stripe;
use Stripe\Customer;
use Stripe\PaymentIntent;
use Stripe\PaymentMethod;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Auth;
use App\Models\Transaction;

class BillingController extends Controller
{
protected $stripe;
protected $user;


public function __construct() {
$this->stripe = new \Stripe\StripeClient(config('services.stripe.secret'));
Stripe::setApiKey(config('services.stripe.secret'));
$this->user = Auth::user();
}

protected function attempt($packs) {
$user = Auth::user();

foreach($packs as $value) {
if ($value < 0) {
abort(422);
}
}

$amount = $packs[ 'credits10' ]*1099 +
$packs[ 'credits50' ]*5499 + $packs[ 'credits100' ]*10999
+ $packs[ 'credits1000' ]*101000;

$transaction = new Transaction;
$transaction->credits = $packs['credits10']*1000 +
$packs['credits50']*5000 +
$packs['credits100']*10000 +
$packs['credits1000']*100000;
$transaction->credits_extra =
$packs['credits50']*500 +
$packs['credits100']*1000 +
$packs['credits1000']*15000;

$transaction->user_id = $user->id;
$transaction->charge = $amount;
$transaction->status = 'processing';
$transaction->completed = false;
$transaction->save();
return $transaction;
}

//Expects an array 'packs' representing the amount of each multiple of credits.
//Should validate that all amounts are positive integers in a reasonable range
public function stripeSecret(Request $request) {
$user = Auth::user();
$transaction = $this->attempt($request->packs);

$intent = PaymentIntent::create([
'amount' => $amount,
'currency' => 'usd',
'customer' => $user->customer_id,
'description' => "You have received $total_credits credits.",
'receipt_email' => Auth::user()->email,
'metadata' => ['transaction_id' => $transaction->id]
]);
$transaction->intent_id = $intent->id;

//Save the card as a default if none is set and it was selected
if ($user->payment_method == null && $request->card) {
$this->changeDefaultCard($request->card);
}

$transaction->save();
return $intent->client_secret;
}

public function getCards() {
return PaymentMethod::all([
'customer' => Auth::user()->customer_id,
'type' => 'card'
]);
}

//Adds correct credit amount to the charged user, precise to two decimal places
public function chargeEvent(Request $request) {
$event = \Stripe\Event::constructFrom($request->all());
$charge = $event->data->object;
$transaction = Transaction::where('intent_id', $charge->payment_intent)->first();

if ($event->type == 'charge.succeeded') {
$this->creditUser($transaction->id);
} else {
$transaction->status = $charge->status;
$transaction->save();
}
}

public function changeDefaultCard(String $card) {
$user = Auth::user();
$user->payment_method = $card;
$user->save();
$cards = $this->getCards();

return PaymentMethod::all([
'customer' => Auth::user()->customer_id,
'type' => 'card'
]);
}

public function deleteCard(Request $request) {
$this->stripe->paymentMethods->detach($request->card);

$user = Auth::user();
if ($request->card == $user->payment_method) {
$user->payment_method = null;
$user->save();
}
return ($this->getCards());
}

//Receives a request with a packs. It is an array of each type of credit
//amount to be bought
public function payeer(Request $request) {
$user = Auth::user();
$transaction = $this->attempt($request->packs);
$shopid = config('services.payeer.id');
$secret = config('services.payeer.secret');
$param_key = config('services.payeer.param_key');
$total = $transaction->credits/100 + $transaction->credits_extra/100;
$description = base64_encode("You will receive $total credits.");

$arHash = [$shopid, $transaction->id, $transaction->charge/100, 'USD',
$description];

$params = ['reference' => ['transaction_id' => $transaction->id]];
$key = md5($param_key.$transaction->id);
$encodedParams = @urlencode(base64_encode(openssl_encrypt(
json_encode($params), 'AES-256-CBC', $key, OPENSSL_RAW_DATA
)));
$arHash[] = $encodedParams;
$arHash[] = $secret;

$signature = strtoupper(hash('sha256', implode(':', $arHash)));
$user->paying = true; $user->save();

return [ 'signature' => $signature, 'params' => $encodedParams, 'shop'
=> $shopid, 'transaction' => $transaction->id, 'amount' =>
$transaction->charge/100, 'description' => $description ];
}

//This needs to check the ip of the sender
public function processPayeer(Request $request) {
$allowed = ['185.71.65.92', '185.71.65.189', '149.202.17.210'];
$ipAddress = $request->ip();

if (!in_array($ipAddress, $allowed)){
abort(401);
}

Log::debug('Processing Payeer payment');
Log::debug($request);

$secret = config('services.payeer.secret');
$arHash = [$request->m_operation_id,
$request->m_operation_ps,
$request->m_operation_day,
$request->m_operation_pay_date,
$request->m_shop,
$request->m_orderid,
$request->m_amount,
$request->m_curr,
$request->m_desc,
$request->m_status
];

if (isset($request->m_params)) {
$arHash[] = $request->m_params;
}

$arHash[] = $secret;

$signature = strtoupper(hash('sha256', implode(':', $arHash)));
if ($signature == $request->m_sign && $request->m_status == 'success'){
$this->creditUser((int) $request->m_orderid);
return $request->m_orderid.'|success';
} else {
$transaction = Transaction::find($request->orderid);
$transaction->status = 'error';
$transaction->save();
return $request->m_orderid.'|error';
}
}

//Credits the user of a given transaction id
public function creditUser($transaction_id) {
$transaction = Transaction::find($transaction_id);

if ($transaction->completed) {
abort(422, 'Bad transaction ID');
}

$user = $transaction->user;
$user->credits = $user->credits + $transaction->credits +
$transaction->credits_extra;
$transaction->status = 'completed';
$transaction->completed = true;

$user->save();
$transaction->save();
}

public function pm(Request $request) {
$user = Auth::user();
$account = config('services.pm.account');
$transaction = $this->attempt($request->packs);
$total = $transaction->credits/100 +
$transaction->credits_extra/100;
$description = "You will receive $total credits.";

$user->paying = true; $user->save();

return ['account' => $account, 'transaction' => $transaction->id,
'amount' => $transaction->charge/100, 'description' => $description];
}

//Handler run after PM payment succeds
public function processPM(Request $request) {
$allowed = ['77.109.141.170', '91.205.41.208', '94.242.216.60',
'78.41.203.75'];
$transaction = Transaction::find($request->PAYMENT_ID);
$secret = config('services.pm.secret');

//Check that sender is PM and account the amount was paid to is mine.
if (!in_array($request->ip(), $allowed)){
abort(401);
} else if ($request->PAYEE_ACCOUNT != config('services.pm.account')) {
abort(422);
} else if (!$transaction->complete) {
abort(422);
}

Log::debug('Processing PM payment');
Log::debug($request);

//Would need to be changed if baggage fields are used
$arHash = [$request->PAYMENT_ID,
$request->PAYEE_ACCOUNT,
$request->PAYMENT_AMOUNT,
$request->PAYMENT_UNITS,
$request->PAYMENT_BATCH_NUMBER,
$request->PAYER_ACCOUNT,
strtoupper(md5($secret)),
$request->TIMESTAMPGMT,
];

$signature = strtoupper(md5(implode(':', $arHash)));

if ($signature == $request->V2_HASH){
$this->creditUser((int) $transaction->id);
} else {
abort(422, 'Bad hash');
}
}

public function completePM(Request $request) {
return redirect('/panel/#transaction-complete');
}

public function failPM(Request $request) {
return redirect('/panel/#transaction-failed');
}

}

+ 6
- 0
README.md Zobrazit soubor

@@ -0,0 +1,6 @@

##Code Examples
Some sample code taken from sites I've completed. They're written in
Javascript, PHP, and SCSS. The examples are incomplete and meant for
demonstrative purposes only as I'm not comfortable share complete code of a
live, potentialy profitable project.

+ 745
- 0
main.scss Zobrazit soubor

@@ -0,0 +1,745 @@
@use "vars";

@font-face {
font-family: "PatuaOne";
src: url("../PatuaOne-Regular.ttf") format("opentype");
font-style: normal;
font-weight: normal;
}

html body {
font-family: 'PatuaOne', 'Times New Roman';
margin: 0;
}

section {
position: relative;
padding-top: 3em;
padding-bottom: 3em;
overflow: hidden;
padding-right: 15px;
padding-left: 15px;
}

button {
font-family: 'PatuaOne', 'Times New Roman';
}

textarea {
border: 1px solid grey;
}

h1 {
color: white;
}

h2 {
color: vars.getColor("brand-orange");
text-align: center;
}

h3 {
color: vars.getColor("faded-text");
// text-transform: uppercase;
}

ul {
color: vars.getColor("faded-text");
list-style-type: none;
padding: 0;
}

input, select {
border: 2px solid black;
border-radius: 20px;
padding: 4px;
}

.icon {
width: 25px;
margin: 10px;
}

.note-grey {
color: grey;
font-size: 0.8em;
max-width: 70%;
}

.services-cards li {
padding-bottom: 8px;
&:before {
width: 30px;
height: 30px;
content: url("../images/green-check.svg");
display: inline;
margin-right: 5px;
// top: 30px;
}
}

a {
text-decoration: none;
}

nav {
position: absolute;
width: 100%;
top: 10px;
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: space-between;
// background-color: transparent;
}

nav h1 {
margin: 4px 0 10px 6px;
display: inline-block;
}

nav.info-page h1 {
margin: 4px 0 10px 6px;
display: inline-block;
color: vars.getColor('brand-orange');
}

.formal-font {
font-family: "FreeSans";
}

nav #logo {
margin-right: 50px;
}

nav form {
display: flex;
flex-wrap: wrap;
margin-right: 20px;
}

nav form div {
margin: 0 10px;
}

nav form button {
min-width: 5em;
}

nav form p {
margin-top: 0;
}

nav form input,label {
opacity: 0.7;
}

nav .nav-toggle {
position: absolute;
right: 10px;
top: 5px;
display: none;
height: 45px;
width: 45px;
margin-left: auto;
background: url("../images/menu-icon.svg");
background-size: 100%;
background-size: cover;
color: white;
}

nav .nav-toggle.toggled {
background: url("../images/cancel-icon2.svg");
width: 25px;
height: 25px;
background-size: 100%;
background-size: cover;
top: 10px;
right: 15px;
color: white;
}

div.landing-hero {
top: 0;
height: 50em;
background-color: #ff4e00;
// background-image: linear-gradient(to right bottom, #ff4e00, #fc6200, #f87200, #f58100, #f18e00, #f19507, #f09c0f, #f0a317, #f3a620, #f6a927, #f8ad2e, #fbb034);
background-image: linear-gradient(315deg, #ff4e00 0%, #ec9f05 74%);
z-index: 0;
}

div.hero-filter {
width: 100%;
height: 100%;
}

div.hero-filter h2 {
color: black;
opacity: 0.7;
max-width: 10em;
font-size: 2.4em;
position: absolute;
top: 3em;
margin-left: 5%;
}

div.hero-filter p {
position: absolute;
top: 12em;
margin-left: 5%;
font-size: 20px;
width: 10em;
opacity: 0.7;
}

div.errors {
background-color: #ececec;
color: vars.getColor("red-alert");
border: 2px solid vars.getColor("red-alert");
border-radius: 4px;
min-height: 50px;
position: absolute;
left: 50%;
margin-right: -50%;
top: 100px;
min-width: 30em;
transform: translate(-50%, -50%);
}

.errors ul {
margin: auto;
}

form.login {
// font-weight: bold;
color: white;
}

.cancel-icon {
width: 30px;
}

form.login input {
margin-left: 2px;
color: white;
background-color: transparent;
border: 2px solid white;
border-radius: 2px;
outline-color: orange
}

form.login .login-btn {
@include vars.special-button("medium-blue", "dark-blue");
}

.hero-filter .register-btn {
@include vars.transparent-button;
display: block;
position: relative;
width: 7em;
top: 60%;
margin-left: auto;
margin-right: auto;
}

div.blue-background {
background-image: url("../images/black-circle.svg");
background-size: 10px;
background-color: #0d324d;
}

section.about-us {
padding: 70px 0;
}

section.about-us p {
max-width: 600px;
margin-left: auto;
margin-right: auto;
border-top: 2px solid vars.getColor("grey");
border-bottom: 2px solid vars.getColor("grey");
padding: 15px;
background-color: #0d324d;
color: white;
}

section.services-cards {
// margin-bottom: 0;
background-color: lightgrey;
padding-bottom: 4em;
background: linear-gradient(to bottom right, transparent 0%, transparent 50%, #e9e9e9 50%, vars.getColor("light-grey") 100%);
}
section.services-cards .cards{
display: flex;
flex-flow: wrap;
margin-top: 2em;
gap: 25px;
justify-content: center;
max-width: 1050px;
margin-left: auto;
margin-right: auto;
text-align: center;
}

section.services-cards h2 {
text-align: center;
padding: 10px;
margin-top: 0;
padding-top: 30px;
color: white;
}

section.services-cards .card {
width: 280px;
height: 350px;
padding: 18px 9px;
border-radius: 4px;
background: white;
box-shadow: 7px 10px 8px rgb(156 166 175 / 22%);
transition: transform 0.2s;
&:hover {
transform: translateY(-20px);
}

}

section.services-cards img {
width: 50px;
height: 50px;
}

section.services-cards button {
@include vars.brand-button("orange");
}

section.features-info {
// height: fit-content;
height: 70em;
background: radial-gradient(ellipse at left, #fff, vars.getColor("light-grey") 80%);
}

.circle-prop {
height: 70em;
overflow: hidden;
width: 100%;
position: relative;
// transform: translateX(-10em);
}

.circle-prop svg {
width: 50%;
}

.features-info img {
width: 600px;
position: absolute;
top: 15%;
right: 4%;
}

div.info-cards {
position: absolute;
top: 10em;
margin-left: 20%;
width: 70%;
}

.info-card {
margin-bottom: 6em;

&:nth-of-type(2) {
margin-left: 13%;
}

&:nth-of-type(3) {
margin-left: 10%;
}

&:nth-of-type(4) {
margin-left: 5%;
}

p {
max-width: 20em;
background: white;
padding: 5px;
border-radius: 4px;
text-align: center;
color: vars.getColor("faded-text2");
// border: 2px solid vars.getColor("medium-orange");
@include vars.hovering;

}
}

section.panel-infos {
min-height: 40em;
margin-top: 5em;
margin-bottom: 5em;
display: flex;
flex-flow: wrap;
align-items: center;
justify-content: center;
gap: 20px;

.panel-preview {
// box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
@include vars.hovering;
padding: 0;
}
}

.panel-infos h2 {
width: 100%;
}


.panel-info {
max-width: 25em;
margin-top: 4em;
}

.panel-info img {
width: 50px;
height: 50px;
}

.panel-info p {
color: vars.getColor("faded-text2");
}

section.benefits-info div {
margin: 1em auto;
padding: 1em 1em;
border-top: 2px solid vars.getColor("faded-text2");
border-bottom: 2px solid vars.getColor("faded-text2");
max-width: 40em;
color: vars.getColor("faded-text");
}

.benefits-info h2 {
margin-bottom: 2em;
}

section.faq-info {
margin-bottom: 6em;
margin-top: 3em;
min-height: 30em;
}

.faq-info h2 {
margin-bottom: 2em;
}

section.faq-info .collapsibles {
display: flex;
flex-flow: wrap;
gap: 2em;
justify-content: center;
max-width: 65em;
width: fit-content;
margin-left: auto;
margin-right: auto;
align-text: center;
}

.faq-info .collapsible {
text-align: center;
width: 20em;
&:not(.active) .content {
display: none;
}
}

.collapsible button {
width: 95%;
display: block;
margin-right: auto;
height: 2.5em;
border: 2px solid vars.getColor("brand-orange");
@include vars.button("light-grey");
font-size: 17px;
// font-weight: bold;
}

.collapsible button:after {
content: '\02795'; /* Unicode character for "plus" sign (+) */
font-size: 13px;
color: white;
float: right;
margin-left: 5px;
}


.collapsible .content {
// background-color: vars.getColor("faded-text2");
border: 2px solid black;
border-radius: 4px;
padding: 4px;
// transition: all 0.2s;
// height: 1px;
}

footer {
height: 6em;
background-color: vars.getColor("dark-grey");
padding: 20px;
color: vars.getColor("faded-text2");
}

footer .foot-links {
width: 20em;
margin: 2em auto;
text-align: center;
}

footer a {
margin: 0 8px;
color: vars.getColor("faded-text2");
&:hover {
color: vars.getColor("faded-text");
}
}

.hidden {
display: none;
}

.loading-icon {
display: block;
width: 50px;
height: 50px;
margin: auto;
}

.register-area p {
text-align: center;
}

.register-area .loading-icon {
margin-top: 100px;
}

div.register-area {
overflow: hidden;
height: 0px;
width: 100%;
z-index: 1;
@include vars.hovering3;
margin-bottom: 2em;
position: fixed;
border-radius: 0;
padding: 0;
opacity: 0;
transition: all 0.2s;
background: white;
h3 {
text-align: center;
}

label,input {
color: black;
display: block;
border-color: black;
width: 95%;
}

div {
margin-top: 20px;
margin-bottom: 20px;
width: 100%;
}

form {
width: 160px;
margin: 20px auto;
}

.submit-btn {
width: 100%;
background-color: #3bb78f;
@include vars.special-button("light-green", "dark-green");
}

.cancel-button {
width: 20px;
height: 20px;
background: url("../images/close-icon-black.svg");
background-size: cover;
position: absolute;
top: 20px;
right: 20px;
}
}

section.features-info {
// min-height: 90em;
}

div.register-area.active {
height: fit-content;
min-height: 20em;
opacity: 1;
}

.medium-icon {
width: 50px;
height: 50px;
margin-left: auto;
margin-right: auto;
display: block;
top: 50px;
}

// Make small screens more usable
@media (max-width: 720px) {
nav { display: block; }

nav form {
// display: none;
width: 190px;
margin-left: auto;
margin-right: auto;
align-items: center;
margin-top: 5em;
opacity: 0;
transition: transform 0.3s;
transform: translateY(-50px);
}

nav form.active {
display: block;
transform: translateX(0%);
opacity: 1;
// background: white;
// color: black;
}

nav form div {
margin: 10px auto;
}

nav div label,input {
display: block;
}

nav .nav-toggle {
display: block;
}

div.register-area.active {
display: initial;
}

div.register-area h1 {
font-size: 1.5rem;
display: inline-block;
color: vars.getColor("brand-orange");
}

div.register-area h3 {
text-align: center;
}

div.register-area .login {
color: vars.getColor("brand-orange");
display: block;
margin-left: auto;
margin-right: auto;
// margin-top: 1em;
width: 10em;
// top: 40px;
// position: absolute;
}

div.register-area .login input {
border: 2px solid black;
border-color: black;
color: black;
}

div.register-area button {
margin-top: 1em;
width: 6em;
}

div.register-area div {
margin-top: 10px;
margin-bottom: 5px;
margin-left: auto;
margin-right: auto;
}

.landing-hero div.hero-filter h2 {
top: 4em;
}

.landing-hero div.hero-filter p {
top: 15em;
}

.landing-hero div.hero-filter .register-btn {
top: 75%;
}
}

.info-heading {
text-align: center;
margin-top: 3em;
margin-left: auto;
margin-right: auto;
max-width: 35em;
padding: 3em;
height: 30em;
button {
display: block;
margin: 3em auto;
width: 6em;
@include vars.special-button("light-green", "dark-green");
}
}

main.panel {
// min-height: 750px;
height: 100vh;
// max-height: 100vh;
background-image: linear-gradient(315deg, #ff4e00 0%, #ec9f05 74%);
}

#panel {
top: 50px;
padding: 0 5%;
margin: auto;
position: relative;
max-width: 800px;
height: 90%;
display: flex;
gap: 10px;
}

#sidebar {
position: relative;
width: 6%;
max-width: 55px;
height: 50%;
color: white;
background: transparent;
display: block;
flex-shrink: 0;

a {
display: block;
margin: 20px 0;
}

svg {
width: 100%;
height: 35px;
color: white;
}

a.selected svg {
color: vars.getColor("brand-orange");
}
}


+ 129
- 0
panel.vue Zobrazit soubor

@@ -0,0 +1,129 @@
<template>
<template v-if="!loading">
<sidebar :role="user.role" :active="active"></sidebar>
<transition name="fade" mode="out-in">
<div v-if="active === ''" id="main">
<section class="welcome-pane"><h3>Welcome, {{user.name}}!</h3></section>
<section class="credits-pane"><img src="../../images/coin-stack.svg"
alt="wallet" class="icon"/><p>Credits:
{{(user.credits/100).toLocaleString('en')}}</p></section>
<section class="alerts-pane"><h4>News and Announcements</h4>
<p>We've just launched. Thanks for joining us! Some features are still
being tested.</p>
</section>
<section class="recent-pane"><h4>Recent Activity</h4>
<table>
<thead><th>Date</th><th>Name</th><th>Status</th></thead>
<tbody>
<tr v-bind:key='order.id' v-for='order in orders.slice(0, 10)'>
<template v-if="order.status != 'pending'">
<td>{{order.updated_at}}</td>
<td>{{order.service.name}}</td>
<td :class="order.status"
class="status"><span>{{order.status.charAt(0).toUpperCase() +
order.status.slice(1)}}</span></td>
</template>
</tr>
</tbody>
</table>
</section>
</div>

<past-orders :token="token" :orders="orders" v-else-if="active === '#orders'"
id="main">
</past-orders>

<new-order :preferred="user.payment_method" :token="token" :active="active"
:credits="user.credits" v-else-if="active ===
'#new-order' || active === '#credits'" id="main" @update-user='getUser'
@update-orders='getOrders'>
</new-order>

<div id="main" v-else-if="active === '#exit'">
<section class="logout-pane">
<h3>Are you sure you want to logout?</h3>
<a href="/logout">Logout</a>
</section>
</div>

<settings :token="token" :user="user" class="settings-page" id="main"
v-else-if="active === '#settings'">
</settings>

<transaction-end @purchase-complete="getUser" :token="token" :user="user"
:active="active" v-else-if="active ==
'#transaction-complete' || active == '#transaction-failed'">
</transaction-end>

<support v-else-if="active == '#support'" :user="user" :token="token"></support>

</transition>
</template>
</template>

<script>
import Sidebar from './sidebar.vue'
import Settings from './settings.vue'
import PastOrders from './orders.vue'
import NewOrder from './services.vue'
import TransactionEnd from './transaction-endpoint.vue'
import Support from './support.vue'

function getServices() {
return fetch("/panel/services", {
method: 'GET',
headers: {'Content-Type': 'application/json',
'Accept': 'application/json',
'X-XSRF-TOKEN': this.token}
}).then(response => {
response.json().then(data => {this.services = data})
})
}

function getUser() {
return fetch("/panel/user", {
method: 'GET',
headers: {'Content-Type': 'application/json',
'Accept': 'application/json',
'X-XSRF-TOKEN': this.token},
}).then(response => {
return response.json()
}).then(data => {
this.user = data
})
}

function getOrders() {
return fetch("/panel/orders", {
method: 'GET',
headers: {'Content-Type': 'application/json',
'Accept': 'application/json',
'X-XSRF-TOKEN': this.token},
}).then(response => {
return response.json()
}).then(data => {
this.orders = data
})
}

export default {
components: {
Sidebar, Settings, PastOrders, NewOrder,
TransactionEnd, Support
},
data() {
return {active: window.location.hash, user: null,
token: null, orders: null, loading: true,}
},
methods: {getUser, getServices, getOrders, },
created() {
let loaders = []
loaders.push(this.getUser())
loaders.push(this.getServices())
loaders.push(this.getOrders())
Promise.all(loaders).then(() => {
this.loading = false
})
}
}
</script>

Načítá se…
Zrušit
Uložit