Compare commits

..

No commits in common. "65e87129a6a33943b7792aba249b2c8fb65b0fc6" and "df53fb7990e1a3e1c08e2f7803a24c005b1e1a1d" have entirely different histories.

61 changed files with 1220 additions and 459 deletions

19
.vscode/launch.json vendored
View File

@ -13,27 +13,10 @@
{ {
"name": "Attach to Remote", "name": "Attach to Remote",
"type": "go", "type": "go",
"debugAdapter": "dlv-dap",
"request": "attach", "request": "attach",
"mode": "remote", "mode": "remote",
"port": 4321, "port": 4321,
"host": "losandesgames.com", "host": "45.76.84.7",
// "substitutePath": [
// { "from": "${workspaceFolder}", "to": "/home/alfheim/linux_amd64" },
// ]
},
{
"name": "Launch Remote",
"type": "go",
"debugAdapter": "dlv-dap",
"request": "launch",
"mode": "exec",
"program": "/home/alfheim/linux_amd64/alfheim-website",
"port": 4321,
"host": "losandesgames.com",
// "substitutePath": [
// { "from": "${workspaceFolder}", "to": "/home/alfheim/linux_amd64" },
// ]
} }
] ]
} }

View File

@ -1,29 +1,32 @@
svn_revision = $(shell svn info --show-item revision)
#LDFLAGS = "-X main.version=$(svn_revision)"
build: build:
@echo "Building the website..." @echo "Building the website..."
rm -rf bin rm -rf bin
mkdir bin mkdir bin
go build -ldflags=$(LDFLAGS) -o bin/alfheim-website ./cmd/web mkdir bin/static
mkdir bin/ui
go build -o bin/alfheim-website
cp -r ui bin cp -r ui bin
cp -r static bin
cp favicon.ico bin
cp Caddyfile bin cp Caddyfile bin
mkdir bin/linux_amd64 mkdir bin/linux_amd64
GOOS=linux GOARCH=amd64 go build -ldflags=$(LDFLAGS) -o bin/linux_amd64/alfheim-website ./cmd/web mkdir bin/linux_amd64/static
mkdir bin/linux_amd64/ui
GOOS=linux GOARCH=amd64 go build -o bin/linux_amd64/alfheim-website
cp -r ui bin/linux_amd64 cp -r ui bin/linux_amd64
cp -r static bin/linux_amd64
cp favicon.ico bin/linux_amd64
cp Caddyfile bin/linux_amd64 cp Caddyfile bin/linux_amd64
run: run:
@echo "Running the website..." @echo "Running the website..."
go run ./cmd/web/*.go go run ./*.go
.PHONY: production/deploy .PHONY: deploy
production/deploy: deploy:
rsync -rP --delete bin/linux_amd64 alfheim@alfheimgame.com:/home/alfheim rsync -rP --delete bin/linux_amd64 alfheim@alfheimgame.com:/home/alfheim
rsync -P remote/production/alfheim-website.service alfheim@alfheimgame.com:/home/alfheim
ssh -t root@alfheimgame.com 'mv /home/alfheim/alfheim-website.service /etc/systemd/system && systemctl enable alfheim-website && systemctl restart alfheim-website'
.PHONY: production/connect .PHONY: service
production/connect: service:
ssh alfheim@losandesgames.com rsync -P alfheim-website.service alfheim@alfheimgame.com:/home/alfheim
ssh -t root@alfheimgame.com 'mv /home/alfheim/alfheim-website.service /etc/systemd/system && systemctl enable alfheim-website && systemctl restart alfheim-website'

View File

@ -15,8 +15,8 @@ CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_BIND_SERVICE
EnvironmentFile=/etc/environment EnvironmentFile=/etc/environment
WorkingDirectory=/home/alfheim/linux_amd64 WorkingDirectory=/home/alfheim/linux_amd64
ExecStart=/home/alfheim/go/bin/dlv --listen=:4321 --headless=true --log=true exec /home/alfheim/linux_amd64/alfheim-website -- -production #ExecStart=/go/bin/dlv --listen=:4321 --headless=true --log=true exec /home/alfheim/linux_amd64/alfheim-website -- -production
#ExecStart=/home/alfheim/linux_amd64/alfheim-website -production ExecStart=/home/alfheim/linux_amd64/alfheim-website -production
Restart=on-failure Restart=on-failure
RestartSec=5 RestartSec=5

7
bin/Caddyfile Normal file
View File

@ -0,0 +1,7 @@
{
email vicenteferrarismith@gmail.com
}
alfheimgame.com {
reverse_proxy localhost:4000
}

BIN
bin/alfheim-website Executable file

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

@ -0,0 +1,7 @@
{
email vicenteferrarismith@gmail.com
}
alfheimgame.com {
reverse_proxy localhost:4000
}

BIN
bin/linux_amd64/alfheim-website Executable file

Binary file not shown.

BIN
bin/linux_amd64/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 279 B

After

Width:  |  Height:  |  Size: 279 B

View File

Before

Width:  |  Height:  |  Size: 173 B

After

Width:  |  Height:  |  Size: 173 B

View File

@ -3,7 +3,7 @@
<div>Username: {{.Account.Username}}</div> <div>Username: {{.Account.Username}}</div>
<div>First name: {{.Account.Firstname}}</div> <div>First name: {{.Account.Firstname}}</div>
<div>Last name: {{.Account.Lastname}}</div> <div>Last name: {{.Account.Lastname}}</div>
<div>Colour: {{.Account.Colour}}</div> <div>Color: {{.Account.Color}}</div>
</div> </div>
<div class="wrapper"> <div class="wrapper">

Binary file not shown.

BIN
bin/static/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M480-120v-80h280v-560H480v-80h280q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H480Zm-80-160-55-58 102-102H120v-80h327L345-622l55-58 200 200-200 200Z"/></svg>

After

Width:  |  Height:  |  Size: 279 B

BIN
bin/static/panel-000.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

202
bin/static/style.css Normal file
View File

@ -0,0 +1,202 @@
html {
/*background: no-repeat url(/static/image.png);
background-size: cover;
background-position: center;*/
height: 100vh;
background-color: black;
box-sizing: border-box;
}
body {
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #3475CB;
font-family: "Vollkorn";
color: white;
margin-top: 0px;
margin-bottom: 0px;
margin-left: auto;
margin-right: auto;
max-width: 900px;
min-height: 100%;
}
header {
}
nav {
padding-left: 8px;
padding-right: 8px;
display: flex;
align-items: center;
justify-content: space-between;
}
.navbuttons a {
display: inline-block;
}
.maintitle {
margin: 0px;
font-size: 6rem;
color: white;
}
@media (max-width: 1080px) {
.maintitle {
font-size: 4rem;
}
}
.loginbutton {
align-self: center;
display: flex;
border: 12px solid;
border-image-source: url("/static/panel-000.png");
border-image-slice: 12 fill;
padding-top: 15px;
padding-right: 20px;
padding-bottom: 15px;
padding-left: 20px;
color: black;
/*box-shadow: 10px 10px 5px lightblue;*/
}
main {
padding: 8px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100%;
max-height: 100%;
margin-left: auto;
margin-right: auto;
}
p {
text-align: center;
font-size: 1rem;
}
.account-wrapper {
background: transparent;
border: 2px solid white;
backdrop-filter: blur(20px);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: 15px;
padding: 30px 40px;
font-size: 1.5rem;
}
.wrapper {
display: inline-block;
background: transparent;
border: 2px solid white;
backdrop-filter: blur(20px);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: 15px;
padding: 30px 40px;
width: 80%;
text-align: center;
}
.wrapper h1 {
font-size: 1.5rem;
text-align: center;
}
.wrapper .input-box {
width: 100%;
}
.input-box input {
box-sizing: border-box;
background: transparent;
width: 100%;
height: 100%;
outline: none;
border: 2px solid rgba(255, 255, 255, .2);
border-radius: 25px;
padding: 10px 45px 10px 20px
}
.input-box input::placeholder {
color: white;
}
.wrapper .input-error {
width: 100%;
}
.error {
color: rgba(240, 0, 0, .8);
}
.input-error input {
box-sizing: border-box;
background: transparent;
width: 100%;
height: 100%;
outline: none;
border: 2px solid rgba(255, 0, 0, .2);
border-radius: 25px;
padding: 10px 45px 10px 20px
}
.input-error input::placeholder {
color: red;
}
.login-btn-wrapper {
display: flex;
justify-content: center;
}
.wrapper .btn {
width: 100%;
outline: none;
border: none;
border-radius: 20px;
}
.wrapper .register-link {
display: flex;
justify-content: center;
}
input {
font-family: inherit;
}
footer {
/*margin-top: auto;*/
width: 100%;
background-color: #132123;
color: white;
text-align: center;
}
a:link {
text-decoration: none;
}
.wrapper a:link {
text-decoration: none;
}
.wrapper a:visited {
text-decoration: none;
}
.wrapper a:hover {
text-decoration: underline;
}
video {
width: 100%;
height: auto;
}

BIN
bin/static/video.mp4 Normal file

Binary file not shown.

22
bin/ui/account.html Normal file
View File

@ -0,0 +1,22 @@
{{define "body"}}
<div class="account-wrapper">
<div>Username: {{.Account.Username}}</div>
<div>First name: {{.Account.Firstname}}</div>
<div>Last name: {{.Account.Lastname}}</div>
<div>Color: {{.Account.Color}}</div>
</div>
<div class="wrapper">
<form action="/deleteaccount" method="post">
<div class="login-btn-wrapper">
<input type="submit" value="Delete Account" class="btn">
</div>
</form>
<form method="POST" action="/managebilling">
<div class="login-btn-wrapper">
<input type="submit" value="Manage Billing" class="btn">
</div>
</form>
</div>
{{end}}

48
bin/ui/base.html Normal file
View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Alfheim</title>
<meta charset="utf-8" />
<meta name="description" content="A handcrafted economy and politics MMO." />
<meta name="author" content="Vicente Ferrari Smith" />
<meta name="keywords" content="Alfheim, indie, video game, mmo, colony, colony simulator, vicente ferrari smith, game, economy, politics, alfheim" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
@font-face {
font-family: "Vollkorn";
src: url(/static/Vollkorn-VariableFont_wght.ttf);
}
</style>
<link href="/static/style.css" rel="stylesheet" />
</head>
<body>
<header>
<nav>
<a href="/"><h1 class="maintitle">Alfheim</h1></a>
<div class="navbuttons">
{{if not .ActiveSubscription}}
<a href="/subscribe"><div class="loginbutton"><strong>Subscribe</strong></div></a>
{{end}}
{{if .AuthenticatedUser}}
<a href="/account"><div class="loginbutton"><strong>Account</strong></div></a>
<a href="/logout"><div class="loginbutton"><strong>Log out</strong></div></a>
{{else}}
<a href="/login"><div class="loginbutton"><strong>Log in</strong><img src="/static/login_24dp_FILL0_wght400_GRAD0_opsz24.svg" alt=""/></div></a>
<a href="/register"><div class="loginbutton"><strong>Register</strong></div></a>
{{end}}
</div>
</nav>
</header>
<main>
{{template "body" .}}
</main>
<footer>
Alfheim &copy; 2025, Vicente Ferrari Smith, Los Andes Games, Vienna, Austria. All rights reserved.
</footer>
</body>
</html>

9
bin/ui/index.html Normal file
View File

@ -0,0 +1,9 @@
{{define "body"}}
<p>Alfheim is a game about humanity: our own will, and our place in the world at large.</p>
<p>Alfheim is an MMO where you&mdash;the player&mdash;administer a medieval settlement of elves.
In an infinite world, and with thousands of other players, you will have to design an efficient command economy, if you wish to be able to compete with your enemies... Or your friends?</p>
<br \>
<video playsinline loop autoplay muted><source src="/static/video.mp4" type="video/mp4">Your browser does not support the video tag.</video>
<br \>
<img src="/static/image.png" alt="Alfheim" style="height: 100%; width: 100%; object-fit: contain">
{{end}}

39
bin/ui/login.html Normal file
View File

@ -0,0 +1,39 @@
{{define "body"}}
<div class="wrapper">
<form method="POST">
<h1>Log in</h1>
{{with .FormErrors.generic}}
<label class="error">{{.}}</label>
{{end}}
{{with .FormErrors.username}}
<label class="error">{{.}}</label>
{{end}}
<div {{if .FormErrors.username}}class="input-error"{{else}}class="input-box"{{end}}>
<input type="text" id="username" name="username" placeholder="Username" required>
</div>
<br />
{{with .FormErrors.password}}
<label class="error">{{.}}</label>
{{end}}
<div {{if .FormErrors.password}}class="input-error"{{else}}class="input-box"{{end}}>
<input type="password" id="password" name="password" placeholder="Password" required>
</div>
<br />
<div class="login-btn-wrapper">
<input type="submit" value="Log in" class="btn">
</div>
<br />
<a href="/forgotten">Have you forgotten your password?</a>
<div class="register-link">
<p>Don't have an account? <a href="/register">Register</a></p>
</div>
</form>
</div>
{{end}}

11
bin/ui/logout.html Normal file
View File

@ -0,0 +1,11 @@
{{define "body"}}
<div class="wrapper">
<form method="POST">
<h1>Log out</h1>
<div class="login-btn-wrapper">
<input type="submit" value="Log out" class="btn">
</div>
</form>
</div>
{{end}}

45
bin/ui/register.html Normal file
View File

@ -0,0 +1,45 @@
{{define "body"}}
<div class="wrapper">
<form method="POST">
<h1>Register</h1>
{{with .FormErrors.username}}
<label class="error">{{.}}</label>
{{end}}
<div class="input-box" {{with .FormErrors.username}}class="input-error"{{end}}>
<input type="text" id="username" name="username" placeholder="Username" required>
</div>
<br />
<div class="input-box">
<input type="email" id="email" name="email" placeholder="Email" required>
</div>
<br />
<div class="input-box">
<input type="text" id="firstname" name="firstname" placeholder="First Name" required>
</div>
<br />
<div class="input-box">
<input type="text" id="lastname" name="lastname" placeholder="Last Name" required>
</div>
<br />
{{with .FormErrors.password}}
<label class="error">{{.}}</label>
{{end}}
<div class="input-box" {{with .FormErrors.password}}class="input-error"{{end}}>
<input type="password" id="password" name="password" placeholder="Password" required>
</div>
<br />
<div class="login-btn-wrapper">
<input type="submit" value="Register" class="btn">
</div>
</form>
</div>
{{end}}

8
bin/ui/subscribe.html Normal file
View File

@ -0,0 +1,8 @@
{{define "body"}}
{{range .Prices}}
<div class="wrapper">
{{$price := divide .UnitAmountDecimal 100}}
{{.Currency}} {{printf "%.2f" $price}}
</div>
{{end}}
{{end}}

View File

@ -0,0 +1,5 @@
{{define "body"}}
<script async src="https://js.stripe.com/v3/pricing-table.js"></script>
<stripe-pricing-table pricing-table-id="prctbl_1PIaJpKUHKCjyTmcy1ONKQiT" publishable-key="pk_test_51PGebgKUHKCjyTmcyuhj7C5lfHfKFLCxRJ2opoqxL3mGEHSRaMvOHYKKgX4MdFfESW78dssjyunboUcFhg3LTwmn005PmzVIXw" customer-session-client-secret={{.ClientSecret}}>
</stripe-pricing-table>
{{end}}

BIN
favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

20
go.mod
View File

@ -1,22 +1,18 @@
module alfheimgame.com/alfheim module alfheimgame.com/alfheim
go 1.24.2 go 1.22.2
require ( require (
github.com/alexedwards/argon2id v1.0.0 github.com/alexedwards/argon2id v1.0.0
github.com/gorilla/sessions v1.4.0 github.com/gorilla/sessions v1.2.2
github.com/jackc/pgx/v5 v5.7.6 github.com/lib/pq v1.10.9
github.com/stripe/stripe-go/v83 v83.0.0 github.com/stripe/stripe-go/v78 v78.7.0
) golang.org/x/crypto v0.33.0
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
golang.org/x/text v0.29.0 // indirect
) )
require ( require (
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
golang.org/x/crypto v0.42.0 // indirect golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.36.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
) )

53
go.sum
View File

@ -1,63 +1,56 @@
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w= github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw= github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stripe/stripe-go/v78 v78.7.0 h1:TdTkzBn0wB0ntgOI74YHpvsNyHPBijX83n4ljsjXh6o=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stripe/stripe-go/v78 v78.7.0/go.mod h1:GjncxVLUc1xoIOidFqVwq+y3pYiG7JLVWiVQxTsLrvQ=
github.com/stripe/stripe-go/v83 v83.0.0 h1:00HYu/n80zH6ugy88bWI5sBLbJZ7WmhCXCRQ1N1tuqI=
github.com/stripe/stripe-go/v83 v83.0.0/go.mod h1:nRyDcLrJtwPPQUnKAFs9Bt1NnQvNhNiF6V19XHmPISE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -65,18 +58,20 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -16,15 +16,13 @@ import (
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/stripe/stripe-go/v83" "github.com/stripe/stripe-go/v78"
"github.com/stripe/stripe-go/v83/billingportal/session" "github.com/stripe/stripe-go/v78/billingportal/session"
"github.com/stripe/stripe-go/v83/customer" "github.com/stripe/stripe-go/v78/customer"
"github.com/stripe/stripe-go/v83/customersession" "github.com/stripe/stripe-go/v78/customersession"
"github.com/stripe/stripe-go/v83/price" "github.com/stripe/stripe-go/v78/price"
"github.com/stripe/stripe-go/v83/subscription" "github.com/stripe/stripe-go/v78/subscription"
"github.com/stripe/stripe-go/v83/webhook" "github.com/stripe/stripe-go/v78/webhook"
"alfheimgame.com/alfheim/pkg/models"
) )
//import "strconv" //import "strconv"
@ -32,7 +30,7 @@ import (
type TemplateData struct { type TemplateData struct {
AuthenticatedUser int32 AuthenticatedUser int32
FormErrors map[string]string FormErrors map[string]string
Account models.Account Account Account
Prices []stripe.Price Prices []stripe.Price
ClientSecret string ClientSecret string
ActiveSubscription bool ActiveSubscription bool
@ -42,7 +40,7 @@ func favicon(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "favicon.ico") http.ServeFile(w, r, "favicon.ico")
} }
func authenticatedUser(w http.ResponseWriter, r *http.Request) int32 { func authenticated_user(w http.ResponseWriter, r *http.Request) int32 {
session, err := store.Get(r, "id") session, err := store.Get(r, "id")
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -79,12 +77,12 @@ func home(w http.ResponseWriter, r *http.Request) {
return return
} }
id := authenticatedUser(w, r) id := authenticated_user(w, r)
account, _ := users.GetAccount(id) account, _ := users.GetAccount(id)
active_subscription := subscriptions.HasActiveSubscription(id) active_subscription := subscriptions.HasActiveSubscription(id)
text, err := template.ParseFiles("ui/html/base.html", "ui/html/index.html") text, err := template.ParseFiles("ui/base.html", "ui/index.html")
if err != nil { if err != nil {
http.Error(w, "Internal Server Error", 500) http.Error(w, "Internal Server Error", 500)
log.Println(err) log.Println(err)
@ -108,7 +106,7 @@ func home(w http.ResponseWriter, r *http.Request) {
} }
func login(w http.ResponseWriter, r *http.Request) { func login(w http.ResponseWriter, r *http.Request) {
text, err := template.ParseFiles("ui/html/base.html", "ui/html/login.html") text, err := template.ParseFiles("ui/base.html", "ui/login.html")
if err != nil { if err != nil {
http.Error(w, "Internal Server Error", 500) http.Error(w, "Internal Server Error", 500)
log.Fatal(err) log.Fatal(err)
@ -143,7 +141,7 @@ func login(w http.ResponseWriter, r *http.Request) {
if len(errors) > 0 { if len(errors) > 0 {
err := text.Execute(w, TemplateData{AuthenticatedUser: authenticatedUser(w, r), FormErrors: errors}) err := text.Execute(w, TemplateData{AuthenticatedUser: authenticated_user(w, r), FormErrors: errors})
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, "Internal Server Error", 500) http.Error(w, "Internal Server Error", 500)
@ -153,9 +151,9 @@ func login(w http.ResponseWriter, r *http.Request) {
} }
id, err := users.Authenticate(username, password) id, err := users.Authenticate(username, password)
if err == models.ErrInvalidCredentials { if err == ErrInvalidCredentials {
errors["generic"] = "Email or Password is incorrect" errors["generic"] = "Email or Password is incorrect"
err := text.Execute(w, TemplateData{AuthenticatedUser: authenticatedUser(w, r), FormErrors: errors}) err := text.Execute(w, TemplateData{AuthenticatedUser: authenticated_user(w, r), FormErrors: errors})
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, "Internal Server Error", 500) http.Error(w, "Internal Server Error", 500)
@ -174,13 +172,13 @@ func login(w http.ResponseWriter, r *http.Request) {
} }
func logout(w http.ResponseWriter, r *http.Request) { func logout(w http.ResponseWriter, r *http.Request) {
text, err := template.ParseFiles("ui/html/base.html", "ui/html/logout.html") text, err := template.ParseFiles("ui/base.html", "ui/logout.html")
if err != nil { if err != nil {
http.Error(w, "Internal Server Error", 500) http.Error(w, "Internal Server Error", 500)
log.Println(err) log.Println(err)
} }
id := authenticatedUser(w, r) id := authenticated_user(w, r)
account, err := users.GetAccount(id) account, err := users.GetAccount(id)
if err != nil { if err != nil {
@ -209,13 +207,13 @@ func logout(w http.ResponseWriter, r *http.Request) {
} }
func register(w http.ResponseWriter, r *http.Request) { func register(w http.ResponseWriter, r *http.Request) {
text, err := template.ParseFiles("ui/html/base.html", "ui/html/register.html") text, err := template.ParseFiles("ui/base.html", "ui/register.html")
if err != nil { if err != nil {
http.Error(w, "Internal Server Error", 500) http.Error(w, "Internal Server Error", 500)
log.Println(err) log.Println(err)
} }
id := authenticatedUser(w, r) id := authenticated_user(w, r)
account, _ := users.GetAccount(id) account, _ := users.GetAccount(id)
// if err != nil { // if err != nil {
@ -233,7 +231,7 @@ func register(w http.ResponseWriter, r *http.Request) {
} }
case http.MethodPost: case http.MethodPost:
account := models.Account{Username: r.FormValue("username"), Password: []byte(r.FormValue("password")), Firstname: r.FormValue("firstname"), Lastname: r.FormValue("lastname"), Email: r.FormValue("email")} account := Account{Username: r.FormValue("username"), Password: []byte(r.FormValue("password")), Firstname: r.FormValue("firstname"), Lastname: r.FormValue("lastname"), Email: r.FormValue("email")}
errors := make(map[string]string) errors := make(map[string]string)
@ -251,7 +249,7 @@ func register(w http.ResponseWriter, r *http.Request) {
if len(errors) > 0 { if len(errors) > 0 {
err := text.Execute(w, TemplateData{AuthenticatedUser: authenticatedUser(w, r), FormErrors: errors}) err := text.Execute(w, TemplateData{AuthenticatedUser: authenticated_user(w, r), FormErrors: errors})
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.Error(w, "Internal Server Error", 500) http.Error(w, "Internal Server Error", 500)
@ -260,7 +258,7 @@ func register(w http.ResponseWriter, r *http.Request) {
_, err := users.Insert(account.Username, string(account.Password), account.Firstname, account.Lastname, account.Email) _, err := users.Insert(account.Username, string(account.Password), account.Firstname, account.Lastname, account.Email)
if err == models.ErrDuplicateEmail || err == models.ErrDuplicateUsername { if err == ErrDuplicateEmail || err == ErrDuplicateUsername {
} else if err != nil { } else if err != nil {
@ -281,7 +279,7 @@ func account(w http.ResponseWriter, r *http.Request) {
// log.Fatal(err) // log.Fatal(err)
//} //}
id := authenticatedUser(w, r) id := authenticated_user(w, r)
account, err := users.GetAccount(id) account, err := users.GetAccount(id)
if err != nil { if err != nil {
@ -291,7 +289,7 @@ func account(w http.ResponseWriter, r *http.Request) {
//log.Println(id, account) //log.Println(id, account)
text, err := template.ParseFiles("ui/html/base.html", "ui/html/account.html") text, err := template.ParseFiles("ui/base.html", "ui/account.html")
if err != nil { if err != nil {
http.Error(w, "Internal Server Error", 500) http.Error(w, "Internal Server Error", 500)
log.Println(err) log.Println(err)
@ -314,9 +312,9 @@ func account(w http.ResponseWriter, r *http.Request) {
// } // }
} }
func deleteAccount(w http.ResponseWriter, r *http.Request) { func deleteaccount(w http.ResponseWriter, r *http.Request) {
id := authenticatedUser(w, r) id := authenticated_user(w, r)
switch r.Method { switch r.Method {
case http.MethodPost: case http.MethodPost:
@ -337,7 +335,7 @@ func deleteAccount(w http.ResponseWriter, r *http.Request) {
} }
func subscribe(w http.ResponseWriter, r *http.Request) { func subscribe(w http.ResponseWriter, r *http.Request) {
id := authenticatedUser(w, r) id := authenticated_user(w, r)
account, err := users.GetAccount(id) account, err := users.GetAccount(id)
if err != nil { if err != nil {
@ -363,7 +361,7 @@ func subscribe(w http.ResponseWriter, r *http.Request) {
}, },
} }
text, err := template.New("base.html").Funcs(fm).ParseFiles("ui/html/base.html", "ui/html/subscribe.html") text, err := template.New("base.html").Funcs(fm).ParseFiles("ui/base.html", "ui/subscribe.html")
if err != nil { if err != nil {
http.Error(w, "Internal Server Error", 500) http.Error(w, "Internal Server Error", 500)
log.Println(err) log.Println(err)
@ -377,8 +375,8 @@ func subscribe(w http.ResponseWriter, r *http.Request) {
} }
} }
func subscribeStripe(w http.ResponseWriter, r *http.Request) { func subscribe_stripe(w http.ResponseWriter, r *http.Request) {
id := authenticatedUser(w, r) id := authenticated_user(w, r)
account, err := users.GetAccount(id) account, err := users.GetAccount(id)
if err != nil { if err != nil {
@ -399,7 +397,7 @@ func subscribeStripe(w http.ResponseWriter, r *http.Request) {
log.Println(err) log.Println(err)
} }
text, err := template.ParseFiles("ui/html/base.html", "ui/html/subscribe_stripe.html") text, err := template.ParseFiles("ui/base.html", "ui/subscribe_stripe.html")
if err != nil { if err != nil {
http.Error(w, "Internal Server Error", 500) http.Error(w, "Internal Server Error", 500)
log.Println(err) log.Println(err)
@ -412,8 +410,8 @@ func subscribeStripe(w http.ResponseWriter, r *http.Request) {
} }
} }
func manageBilling(w http.ResponseWriter, r *http.Request) { func managebilling(w http.ResponseWriter, r *http.Request) {
id := authenticatedUser(w, r) id := authenticated_user(w, r)
account, err := users.GetAccount(id) account, err := users.GetAccount(id)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
@ -421,7 +419,7 @@ func manageBilling(w http.ResponseWriter, r *http.Request) {
params := &stripe.BillingPortalSessionParams{ params := &stripe.BillingPortalSessionParams{
Customer: stripe.String(account.StripeID), Customer: stripe.String(account.StripeID),
ReturnURL: stripe.String("https://alfheimgame.com/account"), ReturnURL: stripe.String("http://localhost:8080/account"),
} }
result, err := session.New(params) result, err := session.New(params)
@ -510,11 +508,11 @@ func webhooks(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
func handle_checkout_session_completed(checkoutSession stripe.CheckoutSession) error { func handle_checkout_session_completed(checkoutsession stripe.CheckoutSession) error {
//toprint, _ := json.MarshalIndent(checkoutSession, "", " ") //toprint, _ := json.MarshalIndent(checkoutSession, "", " ")
//log.Println(string(toprint)) //log.Println(string(toprint))
subscription, err := subscription.Get(checkoutSession.Subscription.ID, nil) subscription, err := subscription.Get(checkoutsession.Subscription.ID, nil)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return err return err
@ -542,19 +540,19 @@ func handle_checkout_session_completed(checkoutSession stripe.CheckoutSession) e
// status = paused // status = paused
//} //}
subscriptions.Insert(checkoutSession.Customer.ID, subscription.ID, checkoutSession.ID, subscription.Status) subscriptions.Insert(checkoutsession.Customer.ID, subscription.ID, checkoutsession.ID, subscription.Status)
return nil return nil
} }
func handlePaymentMethodAttached(paymentMethod stripe.PaymentMethod) error { func handle_payment_method_attached(paymentmethod stripe.PaymentMethod) error {
//toprint, _ := json.MarshalIndent(setupintent, "", " ") //toprint, _ := json.MarshalIndent(setupintent, "", " ")
//log.Println(string(toprint)) //log.Println(string(toprint))
// make this the new customer's default payment method // make this the new customer's default payment method
params := &stripe.CustomerParams{} params := &stripe.CustomerParams{}
params.DefaultSource = &paymentMethod.ID params.DefaultSource = &paymentmethod.ID
/*result*/ _, err := customer.Update(paymentMethod.Customer.ID, params) /*result*/ _, err := customer.Update(paymentmethod.Customer.ID, params)
//log.Println(result) //log.Println(result)
if err != nil { if err != nil {
log.Println(err) log.Println(err)

View File

@ -5,64 +5,48 @@
package main package main
import ( import (
"context"
"flag" "flag"
"fmt"
"log" "log"
"net/http" "net/http"
"database/sql"
"regexp" "regexp"
"github.com/jackc/pgx/v5"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
_ "github.com/lib/pq"
"github.com/stripe/stripe-go/v83" "github.com/stripe/stripe-go/v78"
"alfheimgame.com/alfheim/pkg/models/postgresql"
) )
var users *postgresql.AccountModel var users *Usermodel
var subscriptions *postgresql.SubscriptionModel var subscriptions *SubscriptionModel
var key = []byte("super-secret-key") var key = []byte("super-secret-key")
var store = sessions.NewCookieStore(key) var store = sessions.NewCookieStore(key)
var emailrx = regexp.MustCompile("/^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/") var emailrx = regexp.MustCompile("/^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/")
var version string
func main() { func main() {
addr := flag.String("addr", "127.0.0.1:8080", "HTTP network addr") addr := flag.String("addr", "127.0.0.1:8080", "HTTP network addr")
prodAddr := flag.String("prodaddr", "127.0.0.1:4000", "HTTP network addr") prodaddr := flag.String("prodaddr", "127.0.0.1:4000", "HTTP network addr")
//prodaddr := flag.String("prodaddr", "45.76.84.7:443", "HTTP network addr") //prodaddr := flag.String("prodaddr", "45.76.84.7:443", "HTTP network addr")
production := flag.Bool("production", false, "Whether to use production port and TLS") production := flag.Bool("production", false, "Whether to use production port and TLS")
displayVersion := flag.Bool("version", false, "Display version and exit")
_ = addr _ = addr
flag.Parse() flag.Parse()
if *displayVersion {
fmt.Printf("Version: %s\n", version)
return
}
log.Println("Hello, Sailor!") log.Println("Hello, Sailor!")
stripe.Key = "sk_test_51PGebgKUHKCjyTmc97rfDPcvew6EhqDz2qp3U7XoAMIilAU9IVo2NO4P7ylkTvbBafFVr94trha1VYY32jRWMw2K00Yq7YJXFf" stripe.Key = "sk_test_51PGebgKUHKCjyTmc97rfDPcvew6EhqDz2qp3U7XoAMIilAU9IVo2NO4P7ylkTvbBafFVr94trha1VYY32jRWMw2K00Yq7YJXFf"
store.MaxAge(0) store.MaxAge(0)
db, err := pgx.Connect(context.Background(), "postgres://alfheim:iK2SoVbDhdCki5n3LxGyP6zKpLspt4@losandesgames.com/alfheim") db, err := sql.Open("postgres", "postgres://elves_database:iK2SoVbDhdCki5n3LxGyP6zKpLspt4@80.240.25.87/elves_database")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
defer db.Close()
defer db.Close(context.Background()) users = &Usermodel{db}
subscriptions = &SubscriptionModel{db}
users = &postgresql.AccountModel{DB: db}
subscriptions = &postgresql.SubscriptionModel{DB: db}
mux := http.NewServeMux() mux := http.NewServeMux()
@ -75,7 +59,7 @@ func main() {
//accounts := make([]*Account, 0) //accounts := make([]*Account, 0)
//for rows.Next() { //for rows.Next() {
// acc := new(Account) // acc := new(Account)
// err := rows.Scan(&acc.id, &acc.Username, &acc.password, &acc.Colour) // err := rows.Scan(&acc.id, &acc.Username, &acc.password, &acc.Color)
// if err != nil { // if err != nil {
// log.Fatal(err) // log.Fatal(err)
// } // }
@ -90,7 +74,7 @@ func main() {
// log.Println(acc) // log.Println(acc)
//} //}
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("ui/static")))) mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
mux.HandleFunc("/favicon.ico", favicon) mux.HandleFunc("/favicon.ico", favicon)
@ -98,10 +82,10 @@ func main() {
mux.HandleFunc("/login", login) mux.HandleFunc("/login", login)
mux.HandleFunc("/logout", logout) mux.HandleFunc("/logout", logout)
mux.HandleFunc("/register", register) mux.HandleFunc("/register", register)
mux.HandleFunc("/account", requireAuthenticatedUser(account)) mux.HandleFunc("/account", require_authenticated_user(account))
mux.HandleFunc("/deleteaccount", requireAuthenticatedUser(deleteAccount)) mux.HandleFunc("/deleteaccount", require_authenticated_user(deleteaccount))
mux.HandleFunc("/subscribe", requireAuthenticatedUser(subscribeStripe)) mux.HandleFunc("/subscribe", require_authenticated_user(subscribe_stripe))
mux.HandleFunc("/managebilling", requireAuthenticatedUser(manageBilling)) mux.HandleFunc("/managebilling", require_authenticated_user(managebilling))
mux.HandleFunc("/webhook", webhooks) mux.HandleFunc("/webhook", webhooks)
if *production { if *production {
@ -122,8 +106,8 @@ func main() {
// } // }
// log.Fatal(server.ListenAndServeTLS("", "")) // log.Fatal(server.ListenAndServeTLS("", ""))
log.Fatal(http.ListenAndServe(*prodAddr, logRequest(secureHeaders(mux)))) log.Fatal(http.ListenAndServe(*prodaddr, log_request(secure_headers(mux))))
} else { } else {
log.Fatal(http.ListenAndServe(*addr, logRequest(secureHeaders(mux)))) log.Fatal(http.ListenAndServe(*addr, log_request(secure_headers(mux))))
} }
} }

View File

@ -5,7 +5,7 @@ import (
"net/http" "net/http"
) )
func secureHeaders(next http.Handler) http.Handler { func secure_headers(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) { fn := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-XSS-Protection", "1; mode=block") w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("X-Frame-Options", "deny") w.Header().Set("X-Frame-Options", "deny")
@ -16,7 +16,7 @@ func secureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(fn) return http.HandlerFunc(fn)
} }
func logRequest(next http.Handler) http.Handler { func log_request(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) { fn := func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s - %s %s %s", r.RemoteAddr, r.Proto, r.Method, r.URL) log.Printf("%s - %s %s %s", r.RemoteAddr, r.Proto, r.Method, r.URL)
@ -26,12 +26,12 @@ func logRequest(next http.Handler) http.Handler {
return http.HandlerFunc(fn) return http.HandlerFunc(fn)
} }
func requireAuthenticatedUser(next http.HandlerFunc) http.HandlerFunc { func require_authenticated_user(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// If the user is not authenticated, redirect them to the login page and // If the user is not authenticated, redirect them to the login page and
// return from the middleware chain so that no subsequent handlers in // return from the middleware chain so that no subsequent handlers in
// the chain are executed. // the chain are executed.
if authenticatedUser(w, r) == 0 { if authenticated_user(w, r) == 0 {
http.Redirect(w, r, "/login", http.StatusSeeOther) http.Redirect(w, r, "/login", http.StatusSeeOther)
return return
} }

310
models.go Normal file
View File

@ -0,0 +1,310 @@
//
// Created by vfs on 02.05.2024.
//
package main
import (
"database/sql"
"errors"
"fmt"
"time"
//import "golang.org/x/crypto/bcrypt"
"log"
"github.com/alexedwards/argon2id"
_ "github.com/lib/pq"
"github.com/stripe/stripe-go/v78"
"github.com/stripe/stripe-go/v78/customer"
)
var ErrNoRecord = errors.New("no matching record found")
var ErrInvalidCredentials = errors.New("invalid credentials")
var ErrDuplicateEmail = errors.New("duplicate email")
var ErrDuplicateUsername = errors.New("duplicate username")
type Account struct {
ID int32
Username string
Password []byte
Color int32
Firstname string
Lastname string
Email string
Created time.Time
StripeID string
}
type SubscriptionStatus string
const (
incomplete SubscriptionStatus = "incomplete"
incomplete_expired SubscriptionStatus = "incomplete_expired"
trialing SubscriptionStatus = "trialing"
active SubscriptionStatus = "active"
past_due SubscriptionStatus = "past_due"
canceled SubscriptionStatus = "canceled"
unpaid SubscriptionStatus = "unpaid"
paused SubscriptionStatus = "paused"
)
type Subscription struct {
ID int32
AccountID int32
StripeSubscriptionID string
StripeCheckoutID string
Status SubscriptionStatus
}
type Usermodel struct {
DB *sql.DB
}
type SubscriptionModel struct {
DB *sql.DB
}
func (m *Usermodel) Insert(username string, password string, firstname string, lastname string, email string) (int32, error) {
//hashedpassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
hashedpassword, err := argon2id.CreateHash(password, argon2id.DefaultParams)
if err != nil {
log.Println(err)
return 0, err
}
//log.Println(hashedpassword)
stmt := `INSERT INTO accounts (username, password, firstname, lastname, email, created) VALUES ($1, $2, $3, $4, $5, NOW()) RETURNING id`
var insertid int32
row := m.DB.QueryRow(stmt, username, string(hashedpassword), firstname, lastname, email)
if row.Err() != nil {
log.Println(row.Err())
return 0, row.Err()
}
err = row.Scan(&insertid)
if err != nil {
log.Println(err)
return 0, err
}
params := &stripe.CustomerParams{
Name: stripe.String(fmt.Sprintf("%s %s", firstname, lastname)),
Email: stripe.String(email),
}
customer, err := customer.New(params)
if err != nil {
log.Println(err)
return 0, err
}
stmt = `UPDATE accounts SET stripe_id = $1 WHERE id = $2`
//log.Println(customer.ID, insertid)
_, err = m.DB.Exec(stmt, customer.ID, insertid)
if err != nil {
log.Println(err)
return 0, err
}
return insertid, nil
}
func (m *Usermodel) Delete(id int32) error {
account, err := users.GetAccount(id)
if err != nil {
log.Println(err)
return err
}
if account.StripeID != "" {
/*result*/ _, err := customer.Del(account.StripeID, nil)
if err != nil {
log.Println(err)
}
//log.Println(result)
}
stmt := `DELETE FROM accounts WHERE id = $1`
_, err = m.DB.Exec(stmt, id)
if err != nil {
log.Println(err)
}
return nil
}
func (m *Usermodel) GetAccount(id int32) (Account, error) {
if id == 0 {
return Account{}, ErrNoRecord
}
stmt := `SELECT id, username, password, color, firstname, lastname, email, created, stripe_id FROM accounts WHERE id = $1`
row := m.DB.QueryRow(stmt, id)
var account Account
err := row.Scan(&account.ID, &account.Username, &account.Password, &account.Color, &account.Firstname, &account.Lastname, &account.Email, &account.Created, &account.StripeID)
if err == sql.ErrNoRows {
return Account{}, sql.ErrNoRows
} else if err != nil {
return Account{}, err
}
return account, nil
}
func (m *Usermodel) Authenticate(username string, password string) (int32, error) {
var id int32
var hashedpassword string
row := m.DB.QueryRow("SELECT id, password FROM accounts WHERE username = $1", username)
err := row.Scan(&id, &hashedpassword)
if err == sql.ErrNoRows {
return 0, ErrInvalidCredentials
}
match, err := argon2id.ComparePasswordAndHash(password, hashedpassword)
if !match {
return 0, ErrInvalidCredentials
} else if err != nil {
return 0, err
}
return id, nil
}
func (m *Usermodel) ExistsAccount(id int32) bool {
var exists bool
stmt := `SELECT EXISTS(SELECT 1 FROM accounts WHERE id = $1)`
row := m.DB.QueryRow(stmt, id)
if row.Err() != nil {
log.Println(row.Err())
}
row.Scan(&exists)
//log.Println(exists)
return exists
}
func (m *SubscriptionModel) Insert(stripeid string, stripesubscriptionid string, stripecheckoutid string, status stripe.SubscriptionStatus) (int32, error) {
var id int32
stmt := `SELECT id FROM accounts WHERE stripe_id = $1`
row := m.DB.QueryRow(stmt, stripeid)
if row.Err() != nil {
log.Println(row.Err())
return 0, row.Err()
}
err := row.Scan(&id)
if err != nil {
log.Println(err)
return 0, err
}
stmt = `INSERT INTO subscriptions (account_id, stripe_subscription_id, stripe_checkout_id, status) VALUES ($1, $2, $3, $4::subscription_status) RETURNING id`
var insertid int32
row = m.DB.QueryRow(stmt, id, string(stripesubscriptionid), string(stripecheckoutid), string(status))
if row.Err() != nil {
log.Println(row.Err())
return 0, row.Err()
}
err = row.Scan(&insertid)
if err != nil {
log.Println(err)
return 0, err
}
return insertid, nil
}
func (m *SubscriptionModel) Delete(id int32) error {
stmt := `DELETE FROM accounts WHERE id = $1`
_, err := m.DB.Exec(stmt, id)
if err != nil {
log.Println(err)
return err
}
return nil
}
func (m *SubscriptionModel) GetSubscription(id int32) (Subscription, error) {
if id == 0 {
return Subscription{}, ErrNoRecord
}
stmt := `SELECT id, account_id, stripe_subscription_id, stripe_checkout_id, status FROM subscriptions WHERE id = $1`
row := m.DB.QueryRow(stmt, id)
var subscription Subscription
err := row.Scan(&subscription.ID, &subscription.AccountID, &subscription.StripeSubscriptionID, &subscription.StripeCheckoutID, &subscription.Status)
if err == sql.ErrNoRows {
return Subscription{}, sql.ErrNoRows
} else if err != nil {
return Subscription{}, err
}
//log.Println(subscription.Status)
return subscription, nil
}
func (m *SubscriptionModel) GetSubscriptionsFromAccount(accountid int32) ([]Subscription, error) {
if accountid == 0 {
return nil, ErrNoRecord
}
stmt := `SELECT id, account_id, stripe_subscription_id, stripe_checkout_id, status FROM subscriptions WHERE account_id = $1`
rows, err := m.DB.Query(stmt, accountid)
if err != nil {
return nil, err
}
defer rows.Close()
var subscriptions []Subscription
for rows.Next() {
var subscription Subscription
err := rows.Scan(&subscription.ID, &subscription.AccountID, &subscription.StripeSubscriptionID, &subscription.StripeCheckoutID, &subscription.Status)
if err == sql.ErrNoRows {
return nil, sql.ErrNoRows
} else if err != nil {
return nil, err
}
subscriptions = append(subscriptions, subscription)
}
return subscriptions, nil
}
func (m *SubscriptionModel) HasActiveSubscription(accountid int32) bool {
subscriptions, err := m.GetSubscriptionsFromAccount(accountid)
if err != nil {
return false
}
for _, v := range subscriptions {
if v.Status == active {
return true
} else if v.Status == trialing {
return true
}
}
return false
}

View File

@ -1,48 +0,0 @@
//
// Created by vfs on 02.05.2024.
//
package models
import (
"errors"
"time"
)
var ErrNoRecord = errors.New("no matching record found")
var ErrInvalidCredentials = errors.New("invalid credentials")
var ErrDuplicateEmail = errors.New("duplicate email")
var ErrDuplicateUsername = errors.New("duplicate username")
type Account struct {
ID int32
Username string
Password []byte
Colour int32
Firstname string
Lastname string
Email string
Created time.Time
StripeID string
}
type SubscriptionStatus string
const (
Incomplete SubscriptionStatus = "incomplete"
IncompleteExpired SubscriptionStatus = "incomplete_expired"
Trialing SubscriptionStatus = "trialing"
Active SubscriptionStatus = "active"
PastDue SubscriptionStatus = "past_due"
Canceled SubscriptionStatus = "canceled"
Unpaid SubscriptionStatus = "unpaid"
Paused SubscriptionStatus = "paused"
)
type Subscription struct {
ID int32
AccountID int32
StripeSubscriptionID string
StripeCheckoutID string
Status SubscriptionStatus
}

View File

@ -1,138 +0,0 @@
package postgresql
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5"
"github.com/alexedwards/argon2id"
"github.com/stripe/stripe-go/v83"
"github.com/stripe/stripe-go/v83/customer"
"alfheimgame.com/alfheim/pkg/models"
)
type AccountModel struct {
DB *pgx.Conn
}
func (m *AccountModel) Insert(username string, password string, firstname string, lastname string, email string) (int32, error) {
//hashedpassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
hashedpassword, err := argon2id.CreateHash(password, argon2id.DefaultParams)
if err != nil {
log.Println(err)
return 0, err
}
//log.Println(hashedpassword)
stmt := `INSERT INTO accounts (username, password, firstname, lastname, email) VALUES ($1, $2, $3, $4, $5) RETURNING id`
var insertid int32
err = m.DB.QueryRow(context.Background(), stmt, username, string(hashedpassword), firstname, lastname, email).Scan(&insertid)
if err != nil {
log.Println(err)
return 0, err
}
params := &stripe.CustomerParams{
Name: stripe.String(fmt.Sprintf("%s %s", firstname, lastname)),
Email: stripe.String(email),
}
customer, err := customer.New(params)
if err != nil {
log.Println(err)
return 0, err
}
stmt = `UPDATE accounts SET stripe_id = $1 WHERE id = $2`
_, err = m.DB.Exec(context.Background(), stmt, customer.ID, insertid)
if err != nil {
log.Println(err)
return 0, err
}
return insertid, nil
}
func (m *AccountModel) Delete(id int32) error {
account, err := m.GetAccount(id)
if err != nil {
log.Println(err)
return err
}
if account.StripeID != "" {
/*result*/ _, err := customer.Del(account.StripeID, nil)
if err != nil {
log.Println(err)
}
//log.Println(result)
}
stmt := `DELETE FROM accounts WHERE id = $1`
_, err = m.DB.Exec(context.Background(), stmt, id)
if err != nil {
log.Println(err)
}
return nil
}
func (m *AccountModel) GetAccount(id int32) (models.Account, error) {
if id == 0 {
return models.Account{}, models.ErrNoRecord
}
var account models.Account
stmt := `SELECT id, username, password, colour, firstname, lastname, email, created, stripe_id FROM accounts WHERE id = $1`
err := m.DB.QueryRow(context.Background(), stmt, id).Scan(&account.ID, &account.Username, &account.Password, &account.Colour, &account.Firstname, &account.Lastname, &account.Email, &account.Created, &account.StripeID)
if err == pgx.ErrNoRows {
return models.Account{}, pgx.ErrNoRows
} else if err != nil {
return models.Account{}, err
}
return account, nil
}
func (m *AccountModel) Authenticate(username string, password string) (int32, error) {
var id int32
var hashedpassword string
stmt := `SELECT id, password FROM accounts WHERE username = $1`
err := m.DB.QueryRow(context.Background(), stmt, username).Scan(&id, &hashedpassword)
if err == pgx.ErrNoRows {
return 0, models.ErrInvalidCredentials
}
match, err := argon2id.ComparePasswordAndHash(password, hashedpassword)
if !match {
return 0, models.ErrInvalidCredentials
} else if err != nil {
return 0, err
}
return id, nil
}
func (m *AccountModel) ExistsAccount(id int32) bool {
var exists bool
stmt := `SELECT EXISTS(SELECT 1 FROM accounts WHERE id = $1)`
err := m.DB.QueryRow(context.Background(), stmt, id).Scan(&exists)
if err != nil {
log.Println(err)
}
//log.Println(exists)
return exists
}

View File

@ -1,116 +0,0 @@
package postgresql
import (
"context"
"github.com/jackc/pgx/v5"
"log"
"github.com/stripe/stripe-go/v83"
"alfheimgame.com/alfheim/pkg/models"
)
type SubscriptionModel struct {
DB *pgx.Conn
}
func (m *SubscriptionModel) Insert(stripeid string, stripesubscriptionid string, stripecheckoutid string, status stripe.SubscriptionStatus) (int32, error) {
var id int32
stmt := `SELECT id FROM accounts WHERE stripe_id = $1`
err := m.DB.QueryRow(context.Background(), stmt, stripeid).Scan(&id)
if err != nil {
log.Println(err)
return 0, err
}
stmt = `INSERT INTO subscriptions (account_id, stripe_subscription_id, stripe_checkout_id, status) VALUES ($1, $2, $3, $4::subscription_status) RETURNING id`
var insertid int32
err = m.DB.QueryRow(context.Background(), stmt, id, string(stripesubscriptionid), string(stripecheckoutid), string(status)).Scan(&insertid)
if err != nil {
log.Println(err)
return 0, err
}
return insertid, nil
}
func (m *SubscriptionModel) Delete(id int32) error {
stmt := `DELETE FROM accounts WHERE id = $1`
_, err := m.DB.Exec(context.Background(), stmt, id)
if err != nil {
log.Println(err)
return err
}
return nil
}
func (m *SubscriptionModel) GetSubscription(id int32) (models.Subscription, error) {
if id == 0 {
return models.Subscription{}, models.ErrNoRecord
}
var subscription models.Subscription
stmt := `SELECT id, account_id, stripe_subscription_id, stripe_checkout_id, status FROM subscriptions WHERE id = $1`
err := m.DB.QueryRow(context.Background(), stmt, id).Scan(&subscription.ID, &subscription.AccountID, &subscription.StripeSubscriptionID, &subscription.StripeCheckoutID, &subscription.Status)
if err == pgx.ErrNoRows {
return models.Subscription{}, pgx.ErrNoRows
} else if err != nil {
return models.Subscription{}, err
}
return subscription, nil
}
func (m *SubscriptionModel) GetSubscriptionsFromAccount(accountid int32) ([]models.Subscription, error) {
if accountid == 0 {
return nil, models.ErrNoRecord
}
stmt := `SELECT id, account_id, stripe_subscription_id, stripe_checkout_id, status FROM subscriptions WHERE account_id = $1`
rows, err := m.DB.Query(context.Background(), stmt, accountid)
if err != nil {
return nil, err
}
defer rows.Close()
var subscriptions []models.Subscription
for rows.Next() {
var subscription models.Subscription
err := rows.Scan(&subscription.ID, &subscription.AccountID, &subscription.StripeSubscriptionID, &subscription.StripeCheckoutID, &subscription.Status)
if err == pgx.ErrNoRows {
return nil, pgx.ErrNoRows
} else if err != nil {
return nil, err
}
subscriptions = append(subscriptions, subscription)
}
return subscriptions, nil
}
func (m *SubscriptionModel) HasActiveSubscription(accountid int32) bool {
subscriptions, err := m.GetSubscriptionsFromAccount(accountid)
if err != nil {
return false
}
for _, v := range subscriptions {
switch v.Status {
case models.Active:
return true
case models.Trialing:
return true
}
}
return false
}

Binary file not shown.

BIN
static/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M480-120v-80h280v-560H480v-80h280q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H480Zm-80-160-55-58 102-102H120v-80h327L345-622l55-58 200 200-200 200Z"/></svg>

After

Width:  |  Height:  |  Size: 279 B

BIN
static/panel-000.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

202
static/style.css Normal file
View File

@ -0,0 +1,202 @@
html {
/*background: no-repeat url(/static/image.png);
background-size: cover;
background-position: center;*/
height: 100vh;
background-color: black;
box-sizing: border-box;
}
body {
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #3475CB;
font-family: "Vollkorn";
color: white;
margin-top: 0px;
margin-bottom: 0px;
margin-left: auto;
margin-right: auto;
max-width: 900px;
min-height: 100%;
}
header {
}
nav {
padding-left: 8px;
padding-right: 8px;
display: flex;
align-items: center;
justify-content: space-between;
}
.navbuttons a {
display: inline-block;
}
.maintitle {
margin: 0px;
font-size: 6rem;
color: white;
}
@media (max-width: 1080px) {
.maintitle {
font-size: 4rem;
}
}
.loginbutton {
align-self: center;
display: flex;
border: 12px solid;
border-image-source: url("/static/panel-000.png");
border-image-slice: 12 fill;
padding-top: 15px;
padding-right: 20px;
padding-bottom: 15px;
padding-left: 20px;
color: black;
/*box-shadow: 10px 10px 5px lightblue;*/
}
main {
padding: 8px;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100%;
max-height: 100%;
margin-left: auto;
margin-right: auto;
}
p {
text-align: center;
font-size: 1rem;
}
.account-wrapper {
background: transparent;
border: 2px solid white;
backdrop-filter: blur(20px);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: 15px;
padding: 30px 40px;
font-size: 1.5rem;
}
.wrapper {
display: inline-block;
background: transparent;
border: 2px solid white;
backdrop-filter: blur(20px);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: 15px;
padding: 30px 40px;
width: 80%;
text-align: center;
}
.wrapper h1 {
font-size: 1.5rem;
text-align: center;
}
.wrapper .input-box {
width: 100%;
}
.input-box input {
box-sizing: border-box;
background: transparent;
width: 100%;
height: 100%;
outline: none;
border: 2px solid rgba(255, 255, 255, .2);
border-radius: 25px;
padding: 10px 45px 10px 20px
}
.input-box input::placeholder {
color: white;
}
.wrapper .input-error {
width: 100%;
}
.error {
color: rgba(240, 0, 0, .8);
}
.input-error input {
box-sizing: border-box;
background: transparent;
width: 100%;
height: 100%;
outline: none;
border: 2px solid rgba(255, 0, 0, .2);
border-radius: 25px;
padding: 10px 45px 10px 20px
}
.input-error input::placeholder {
color: red;
}
.login-btn-wrapper {
display: flex;
justify-content: center;
}
.wrapper .btn {
width: 100%;
outline: none;
border: none;
border-radius: 20px;
}
.wrapper .register-link {
display: flex;
justify-content: center;
}
input {
font-family: inherit;
}
footer {
/*margin-top: auto;*/
width: 100%;
background-color: #132123;
color: white;
text-align: center;
}
a:link {
text-decoration: none;
}
.wrapper a:link {
text-decoration: none;
}
.wrapper a:visited {
text-decoration: none;
}
.wrapper a:hover {
text-decoration: underline;
}
video {
width: 100%;
height: auto;
}

BIN
static/video.mp4 Normal file

Binary file not shown.

22
ui/account.html Normal file
View File

@ -0,0 +1,22 @@
{{define "body"}}
<div class="account-wrapper">
<div>Username: {{.Account.Username}}</div>
<div>First name: {{.Account.Firstname}}</div>
<div>Last name: {{.Account.Lastname}}</div>
<div>Color: {{.Account.Color}}</div>
</div>
<div class="wrapper">
<form action="/deleteaccount" method="post">
<div class="login-btn-wrapper">
<input type="submit" value="Delete Account" class="btn">
</div>
</form>
<form method="POST" action="/managebilling">
<div class="login-btn-wrapper">
<input type="submit" value="Manage Billing" class="btn">
</div>
</form>
</div>
{{end}}

48
ui/base.html Normal file
View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Alfheim</title>
<meta charset="utf-8" />
<meta name="description" content="A handcrafted economy and politics MMO." />
<meta name="author" content="Vicente Ferrari Smith" />
<meta name="keywords" content="Alfheim, indie, video game, mmo, colony, colony simulator, vicente ferrari smith, game, economy, politics, alfheim" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
@font-face {
font-family: "Vollkorn";
src: url(/static/Vollkorn-VariableFont_wght.ttf);
}
</style>
<link href="/static/style.css" rel="stylesheet" />
</head>
<body>
<header>
<nav>
<a href="/"><h1 class="maintitle">Alfheim</h1></a>
<div class="navbuttons">
{{if not .ActiveSubscription}}
<a href="/subscribe"><div class="loginbutton"><strong>Subscribe</strong></div></a>
{{end}}
{{if .AuthenticatedUser}}
<a href="/account"><div class="loginbutton"><strong>Account</strong></div></a>
<a href="/logout"><div class="loginbutton"><strong>Log out</strong></div></a>
{{else}}
<a href="/login"><div class="loginbutton"><strong>Log in</strong><img src="/static/login_24dp_FILL0_wght400_GRAD0_opsz24.svg" alt=""/></div></a>
<a href="/register"><div class="loginbutton"><strong>Register</strong></div></a>
{{end}}
</div>
</nav>
</header>
<main>
{{template "body" .}}
</main>
<footer>
Alfheim &copy; 2025, Vicente Ferrari Smith, Los Andes Games, Vienna, Austria. All rights reserved.
</footer>
</body>
</html>

9
ui/index.html Normal file
View File

@ -0,0 +1,9 @@
{{define "body"}}
<p>Alfheim is a game about humanity: our own will, and our place in the world at large.</p>
<p>Alfheim is an MMO where you&mdash;the player&mdash;administer a medieval settlement of elves.
In an infinite world, and with thousands of other players, you will have to design an efficient command economy, if you wish to be able to compete with your enemies... Or your friends?</p>
<br \>
<video playsinline loop autoplay muted><source src="/static/video.mp4" type="video/mp4">Your browser does not support the video tag.</video>
<br \>
<img src="/static/image.png" alt="Alfheim" style="height: 100%; width: 100%; object-fit: contain">
{{end}}

39
ui/login.html Normal file
View File

@ -0,0 +1,39 @@
{{define "body"}}
<div class="wrapper">
<form method="POST">
<h1>Log in</h1>
{{with .FormErrors.generic}}
<label class="error">{{.}}</label>
{{end}}
{{with .FormErrors.username}}
<label class="error">{{.}}</label>
{{end}}
<div {{if .FormErrors.username}}class="input-error"{{else}}class="input-box"{{end}}>
<input type="text" id="username" name="username" placeholder="Username" required>
</div>
<br />
{{with .FormErrors.password}}
<label class="error">{{.}}</label>
{{end}}
<div {{if .FormErrors.password}}class="input-error"{{else}}class="input-box"{{end}}>
<input type="password" id="password" name="password" placeholder="Password" required>
</div>
<br />
<div class="login-btn-wrapper">
<input type="submit" value="Log in" class="btn">
</div>
<br />
<a href="/forgotten">Have you forgotten your password?</a>
<div class="register-link">
<p>Don't have an account? <a href="/register">Register</a></p>
</div>
</form>
</div>
{{end}}

11
ui/logout.html Normal file
View File

@ -0,0 +1,11 @@
{{define "body"}}
<div class="wrapper">
<form method="POST">
<h1>Log out</h1>
<div class="login-btn-wrapper">
<input type="submit" value="Log out" class="btn">
</div>
</form>
</div>
{{end}}

45
ui/register.html Normal file
View File

@ -0,0 +1,45 @@
{{define "body"}}
<div class="wrapper">
<form method="POST">
<h1>Register</h1>
{{with .FormErrors.username}}
<label class="error">{{.}}</label>
{{end}}
<div class="input-box" {{with .FormErrors.username}}class="input-error"{{end}}>
<input type="text" id="username" name="username" placeholder="Username" required>
</div>
<br />
<div class="input-box">
<input type="email" id="email" name="email" placeholder="Email" required>
</div>
<br />
<div class="input-box">
<input type="text" id="firstname" name="firstname" placeholder="First Name" required>
</div>
<br />
<div class="input-box">
<input type="text" id="lastname" name="lastname" placeholder="Last Name" required>
</div>
<br />
{{with .FormErrors.password}}
<label class="error">{{.}}</label>
{{end}}
<div class="input-box" {{with .FormErrors.password}}class="input-error"{{end}}>
<input type="password" id="password" name="password" placeholder="Password" required>
</div>
<br />
<div class="login-btn-wrapper">
<input type="submit" value="Register" class="btn">
</div>
</form>
</div>
{{end}}

8
ui/subscribe.html Normal file
View File

@ -0,0 +1,8 @@
{{define "body"}}
{{range .Prices}}
<div class="wrapper">
{{$price := divide .UnitAmountDecimal 100}}
{{.Currency}} {{printf "%.2f" $price}}
</div>
{{end}}
{{end}}

5
ui/subscribe_stripe.html Normal file
View File

@ -0,0 +1,5 @@
{{define "body"}}
<script async src="https://js.stripe.com/v3/pricing-table.js"></script>
<stripe-pricing-table pricing-table-id="prctbl_1PIaJpKUHKCjyTmcy1ONKQiT" publishable-key="pk_test_51PGebgKUHKCjyTmcyuhj7C5lfHfKFLCxRJ2opoqxL3mGEHSRaMvOHYKKgX4MdFfESW78dssjyunboUcFhg3LTwmn005PmzVIXw" customer-session-client-secret={{.ClientSecret}}>
</stripe-pricing-table>
{{end}}