DEVELOPMENT ENVIRONMENT

~liljamo/tixe

c57a862c4977378256899f12648f72534ee68a32 — Jonni Liljamo 1 year, 2 months ago b76c914 internal-users-1st-draft
wip: internal users
A api/login.go => api/login.go +56 -0
@@ 0,0 1,56 @@
package api

import (
	"context"
	"log"
	"net/http"
	"tixe/db"

	"github.com/gin-gonic/gin"
	"github.com/matthewhartstonge/argon2"
)

type PostInternalLoginDetails struct {
	Username string `form:"username"`
	Password string `form:"password"`
}

func PostInternalLogin(c *gin.Context) {
	userDetails := &PostInternalLoginDetails{}
	if err := c.Bind(userDetails); err != nil {
		log.Print("[tixe/api/iauth/login] Could not bind login details")
		c.String(http.StatusBadRequest, "could not bind login details")
		return;
	}

	// Fetch user
	var (
		username string
		passwordHash string
	)
	
	err := db.PgPool.QueryRow(context.Background(), "SELECT username, password FROM users WHERE username = $1", userDetails.Username).Scan(&username, &passwordHash)
	if err != nil {
		log.Printf("[tixe/api/iauth/login] WARN: Error querying internal user from database: %s", err.Error())
		c.String(http.StatusUnauthorized, "incorrect details")
		return
	}

	// Verify password
	ok, err := argon2.VerifyEncoded([]byte(userDetails.Password), []byte(passwordHash))
	if err != nil {
		log.Printf("[tixe/api/iauth/login] WARN: Error verifying internal user password: %s", err.Error())
		c.String(http.StatusUnauthorized, "incorrect details")
		return
	}

	if !ok {
		// Password did not match
		c.String(http.StatusUnauthorized, "incorrect details")
		return
	}

	

	c.Redirect(http.StatusTemporaryRedirect, "/")
}

M db/migrations.go => db/migrations.go +12 -0
@@ 64,5 64,17 @@ func migrations() []string {
			ran_timestamp timestamp,
			schema_version integer
		)`,migrationsTable),
		`CREATE TABLE users (
			id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
			is_internal BOOLEAN NOT NULL DEFAULT FALSE,
			is_admin BOOLEAN NOT NULL DEFAULT FALSE,
			display_name TEXT NOT NULL,
			username TEXT,
			password TEXT,
			oidc_subject TEXT
		)`,
		`INSERT INTO
		  users(is_internal, is_admin, display_name, username, password)
				VALUES(TRUE, TRUE, "admin", "admin", "$argon2id$v=19$m=65536,t=3,p=4$UC+I4MhJyVJG+N2OGPecHQ$ISwMkf1LQh0wUgoP7im7yzT1vKUNDTWVbT828m75woY")`,
	}
}

M dev.elv => dev.elv +1 -1
@@ 1,4 1,4 @@
#!/bin/elvish
#!/usr/bin/env elvish

fn run {
    clear

A flake.lock => flake.lock +27 -0
@@ 0,0 1,27 @@
{
  "nodes": {
    "nixpkgs": {
      "locked": {
        "lastModified": 1687701825,
        "narHash": "sha256-aMC9hqsf+4tJL7aJWSdEUurW2TsjxtDcJBwM9Y4FIYM=",
        "owner": "nixos",
        "repo": "nixpkgs",
        "rev": "07059ee2fa34f1598758839b9af87eae7f7ae6ea",
        "type": "github"
      },
      "original": {
        "owner": "nixos",
        "ref": "nixpkgs-unstable",
        "repo": "nixpkgs",
        "type": "github"
      }
    },
    "root": {
      "inputs": {
        "nixpkgs": "nixpkgs"
      }
    }
  },
  "root": "root",
  "version": 7
}

A flake.nix => flake.nix +24 -0
@@ 0,0 1,24 @@
{
  description = "tixe";
  nixConfig.bash-prompt = "nix | tixe> ";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
  };

  outputs = { self, nixpkgs }: 
    let
      pkgs = nixpkgs.legacyPackages.x86_64-linux.pkgs;
    in
    {
      devShells.x86_64-linux.default = pkgs.mkShell {
        name = "tixe env";
        buildInputs = with pkgs; [
          go
          gopls

          nodePackages.tailwindcss
        ];
      };
    };
}

M go.mod => go.mod +1 -0
@@ 27,6 27,7 @@ require (
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/klauspost/cpuid/v2 v2.2.5 // indirect
	github.com/leodido/go-urn v1.2.4 // indirect
	github.com/matthewhartstonge/argon2 v0.3.2 // indirect
	github.com/mattn/go-isatty v0.0.19 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect

M go.sum => go.sum +2 -0
@@ 54,6 54,8 @@ github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/q
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/matthewhartstonge/argon2 v0.3.2 h1:nOsWkxRFIdPueV7Dla/R/5JZRN2NncnCAlMg3ktyv/A=
github.com/matthewhartstonge/argon2 v0.3.2/go.mod h1:7SVzBdtpqJcIUzzRghzsT8m/DT/kbTDiGifrjZbQ0sA=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

M handlers/auth.go => handlers/auth.go +29 -0
@@ 1,8 1,11 @@
package handlers

import (
	"context"
	"log"
	"net/http"
	"tixe/auth"
	"tixe/db"

	"github.com/gin-contrib/sessions"
	"github.com/gin-gonic/gin"


@@ 41,6 44,32 @@ func AuthCallback(auth *auth.Auth) gin.HandlerFunc {
			return
		}

		// Create a user in the database for this OIDC user.
		// idToken.Subject shooooould be unique and always the same for a user.
		// I think. Maybe.

		// But first, check if it exists!
		var oidcSubject string
		err = db.PgPool.QueryRow(context.Background(), "SELECT oidc_subject FROM users WHERE oidc_subject = $1", idToken.Subject).Scan(&oidcSubject)
		if err != nil {
			log.Printf("[tixe/auth] WARN: Failed to query database for oidc user")
			c.String(http.StatusInternalServerError, err.Error())
			return
		}
		if idToken.Subject == oidcSubject {
			// Exists, we outta here!
			c.Redirect(http.StatusTemporaryRedirect, "/")
			return
		}

		// Now create it, since it did not exists.
		_, err = db.PgPool.Exec(context.Background(), "INSERT INTO users(display_name, oidc_subject) VALUES($1, $2)", profile["name"].(string), idToken.Subject)
		if err != nil {
			log.Printf("[tixe/auth] WARN: Could not create database entry for oidc user.")
			c.String(http.StatusInternalServerError, err.Error())
			return
		}

		c.Redirect(http.StatusTemporaryRedirect, "/")
	}
}

M static/styles.css => static/styles.css +43 -12
@@ 527,6 527,11 @@ video {
  margin-bottom: 3rem;
}

.my-4 {
  margin-top: 1rem;
  margin-bottom: 1rem;
}

.flex {
  display: flex;
}


@@ 549,14 554,22 @@ video {
  min-width: max-content;
}

.max-w-4xl {
  max-width: 56rem;
.min-w-full {
  min-width: 100%;
}

.max-w-3xl {
  max-width: 48rem;
}

.max-w-4xl {
  max-width: 56rem;
}

.max-w-full {
  max-width: 100%;
}

.grow {
  flex-grow: 1;
}


@@ 589,10 602,18 @@ video {
  border-radius: 0.375rem;
}

.rounded {
  border-radius: 0.25rem;
}

.border-2 {
  border-width: 2px;
}

.border {
  border-width: 1px;
}

.border-b-2 {
  border-bottom-width: 2px;
}


@@ 618,18 639,18 @@ video {
  background-image: linear-gradient(to right, var(--tw-gradient-stops));
}

.from-slate-600 {
  --tw-gradient-from: #475569 var(--tw-gradient-from-position);
  --tw-gradient-to: rgb(71 85 105 / 0) var(--tw-gradient-to-position);
  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}

.from-neutral-500 {
  --tw-gradient-from: #737373 var(--tw-gradient-from-position);
  --tw-gradient-to: rgb(115 115 115 / 0) var(--tw-gradient-to-position);
  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}

.from-slate-600 {
  --tw-gradient-from: #475569 var(--tw-gradient-from-position);
  --tw-gradient-to: rgb(71 85 105 / 0) var(--tw-gradient-to-position);
  --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
}

.from-transparent {
  --tw-gradient-from: transparent var(--tw-gradient-from-position);
  --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position);


@@ 641,14 662,14 @@ video {
  --tw-gradient-stops: var(--tw-gradient-from), #60a5fa var(--tw-gradient-via-position), var(--tw-gradient-to);
}

.to-rose-500 {
  --tw-gradient-to: #f43f5e var(--tw-gradient-to-position);
}

.to-neutral-500 {
  --tw-gradient-to: #737373 var(--tw-gradient-to-position);
}

.to-rose-500 {
  --tw-gradient-to: #f43f5e var(--tw-gradient-to-position);
}

.to-transparent {
  --tw-gradient-to: transparent var(--tw-gradient-to-position);
}


@@ 702,6 723,11 @@ video {
  color: transparent;
}

.text-slate-500 {
  --tw-text-opacity: 1;
  color: rgb(100 116 139 / var(--tw-text-opacity));
}

.opacity-75 {
  opacity: 0.75;
}


@@ 740,6 766,11 @@ video {
  text-decoration-line: underline;
}

.focus\:outline-none:focus {
  outline: 2px solid transparent;
  outline-offset: 2px;
}

@media (prefers-color-scheme: dark) {
  .dark\:opacity-100 {
    opacity: 1;

M template/templates/login.tmpl => template/templates/login.tmpl +20 -3
@@ 7,8 7,25 @@
	Liljamo Auth
</a>
<div class="max-w-3xl w-full flex items-center gap-4">
	<div class="grow my-12 h-px border-t-0 bg-transparent bg-gradient-to-r from-transparent to-neutral-500 opacity-75 dark:opacity-100"></div>
  <div class="grow-0">OR</div>
  <div class="grow my-12 h-px border-t-0 bg-transparent bg-gradient-to-r from-neutral-500 to-transparent opacity-75 dark:opacity-100"></div>
	<div class="grow my-4 h-px border-t-0 bg-transparent bg-gradient-to-r from-transparent to-neutral-500 opacity-75 dark:opacity-100"></div>
  <div class="grow-0">or</div>
  <div class="grow my-4 h-px border-t-0 bg-transparent bg-gradient-to-r from-neutral-500 to-transparent opacity-75 dark:opacity-100"></div>
</div>
<div>
  <form class="flex flex-col gap-2 items-center max-w-full" action="/api/iauth/login" method="POST">
		<div class="flex gap-2 p-2 border rounded min-w-full">
			<label for="username">Username</label>
			<input class="text-slate-500 focus:outline-none" type="text" name="username" id="username" placeholder="Username..."/>
		</div>
		<div class="flex gap-2 p-2 border rounded min-w-full">
			<label for="password">Password</label>
			<input class="text-slate-500 focus:outline-none" type="password" name="password" id="password" placeholder="Password..."/>
		</div>
		<button type="submit" class="w-max p-2 border-2
				rounded-md drop-shadow-md
				bg-slate-50 hover:bg-slate-200 transition-colors">
			Login
		</button>
	</form>
</div>
{{ end }}

M tixe.go => tixe.go +6 -0
@@ 64,6 64,12 @@ func setupRouter(auth *auth.Auth) *gin.Engine {
		{
			apiRoute.GET("/", api.Root)
			apiRoute.GET("/ping", ping)

			// internal auth route
			iauth := apiRoute.Group("/iauth")
			{
				iauth.POST("/login", api.PostInternalLogin)
			}
		}
	}